Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/hooks/lumen_read_intercept.sh
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ case "$EXT" in
rs|py|pyi|ts|tsx)
FILE_TYPE="source"
;;
log|out|txt)
log|out|output|txt)
FILE_TYPE="log"
;;
*)
Expand Down
31 changes: 30 additions & 1 deletion crates/lumen-core/src/coverage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@
pub const SOURCE_EXTS: &[&str] = &["rs", "py", "pyi", "ts", "mts", "cts", "tsx"];

/// Extensions routed to `compress_logs` rather than to an outline.
pub const LOG_EXTS: &[&str] = &["log", "out", "txt"];
///
/// `output` is here because `out` was, and the two are the same kind of file. A build or test run
/// redirected to `something.output` was read whole — 54 recorded reads, 9,547 tokens at or above the
/// threshold — purely because the list stopped one word short. Extension matching is exact, so
/// `.output` never matched `out`.
pub const LOG_EXTS: &[&str] = &["log", "out", "output", "txt"];

/// Extensions the hooks actually intercept today.
///
Expand Down Expand Up @@ -177,6 +182,30 @@ mod tests {
assert_eq!(classify("a.dylib", None), Scope::Unmeasurable);
}

#[test]
fn output_files_are_intercepted_like_every_other_log_kind() {
// `.out` was in the list and `.output` was not, so a build or test run redirected to
// `something.output` was read whole. Extension matching is exact — `.output` never matched
// `out` — which is why one missing word cost 9,547 tokens across 54 recorded reads.
assert_eq!(classify("build.output", Some(500)), Scope::Optimizable);
assert_eq!(classify("/tmp/test.OUTPUT", Some(300)), Scope::Optimizable);
// Still subject to the threshold like anything else.
assert_eq!(classify("build.output", Some(10)), Scope::BelowThreshold);
}

#[test]
fn the_log_kinds_are_the_ones_compress_logs_can_actually_help_with() {
for e in LOG_EXTS {
assert_eq!(
classify(&format!("a.{e}"), Some(500)),
Scope::Optimizable,
"{e} is in LOG_EXTS but is not classified as optimizable"
);
}
// A near-miss that must stay out: `.outputs` is not `.output`.
assert_eq!(classify("a.outputs", Some(500)), Scope::UncoveredKind);
}

#[test]
fn a_large_unhandled_kind_is_the_coverage_backlog_not_a_leak() {
// md/scss/css/js at >=300 lines: real tokens spent, but nothing wrong with the router.
Expand Down
9 changes: 9 additions & 0 deletions crates/lumen-mcp/tests/optimizer_baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"(inline)compress_logs": "e01e50e4310f7465",
"app.log": "938a893edd0f0d39",
"big.rs": "c1c363809b8ec51f",
"build.output": "dfe6d57117e3675d",
"crash.log": "3f323125b2195ba1",
"decls.rs": "cc0c3f86f715224f",
"mod.py": "02488faac89903ab",
Expand All @@ -28,6 +29,14 @@
"route": "compress_logs",
"is_error": false
},
"compress_logs/output_extension": {
"tool": "compress_logs",
"full_tokens": 13842,
"returned_tokens": 1062,
"saved_tokens": 12780,
"route": "compress_logs",
"is_error": false
},
"compress_logs/repetitive_path": {
"tool": "compress_logs",
"full_tokens": 18380,
Expand Down
7 changes: 7 additions & 0 deletions crates/lumen-mcp/tests/optimizer_bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,13 @@ fn scenarios() -> Vec<Scenario> {
fixture: Some(("unique.log", incompressible_log(400))),
args: |p| json!({ "path": p }),
},
Scenario {
id: "compress_logs/output_extension",
tool: "compress_logs",
intent: "a .output file is a log; it was read whole until the list gained the word",
fixture: Some(("build.output", repetitive_log(18, 25))),
args: |p| json!({ "path": p }),
},
Scenario {
id: "compress_logs/inline_text",
tool: "compress_logs",
Expand Down
106 changes: 105 additions & 1 deletion lumenator/src-tauri/src/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1264,7 +1264,7 @@ fi
EXT=$(echo "${FILE_PATH##*.}" | tr '[:upper:]' '[:lower:]')
case "$EXT" in
rs|py|pyi|ts|tsx) FILE_TYPE="source" ;;
log|out|txt) FILE_TYPE="log" ;;
log|out|output|txt) FILE_TYPE="log" ;;
*) exit 0 ;;
esac

Expand Down Expand Up @@ -4521,6 +4521,110 @@ mod tests {
/// appearing to work. A row missing token_source is indistinguishable from a bytes/4
/// estimate, and one missing req_key cannot be deduplicated, so a partial writer
/// quietly degrades every figure built on the ledger.
#[test]
fn the_generated_intercept_actually_redirects_a_large_output_file() {
// End to end through the real script: a .output file above the threshold must be blocked
// and pointed at compress_logs. The unit test above checks the list; this checks that the
// shell built from it behaves.
let h = TempDir::new().unwrap();
let script = h.path().join("intercept.sh");
std::fs::write(
&script,
desired_intercept_script("/tmp/x.db", &h.path().join("mcp").to_string_lossy()),
)
.unwrap();
// The guard needs an executable at LUMEN_MCP_BIN or it fails open before deciding.
let mcp = h.path().join("mcp");
std::fs::write(&mcp, "#!/bin/sh\nexit 0\n").unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&mcp, std::fs::Permissions::from_mode(0o755)).unwrap();
}

let big = h.path().join("build.output");
std::fs::write(&big, "a line of build output\n".repeat(400)).unwrap();

let payload = format!(
r#"{{"tool_name":"Read","tool_input":{{"file_path":"{}"}},"session_id":"out-e2e"}}"#,
big.to_string_lossy()
);
let out = std::process::Command::new("bash")
.arg(&script)
.env("LUMEN_MCP_BIN", &mcp)
.env("TMPDIR", h.path())
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.and_then(|mut c| {
use std::io::Write;
c.stdin.take().unwrap().write_all(payload.as_bytes())?;
c.wait_with_output()
})
.expect("run the intercept");

// Exit 2 is the block: the hook tells Claude Code to use the tool instead.
assert_eq!(
out.status.code(),
Some(2),
"a 400-line .output file must be redirected, not passed through. stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let msg = String::from_utf8_lossy(&out.stderr);
assert!(
msg.contains("compress_logs"),
"an .output file is a log, so it must be sent to compress_logs: {msg}"
);
}

#[test]
fn both_intercept_copies_route_every_log_extension_including_output() {
// The list lives in three places: lumen_core::coverage::LOG_EXTS, INTERCEPT_TEMPLATE here,
// and the repo's developer hook. `.output` was missing from all the shell copies because
// nobody was comparing them to the constant — so this drives the assertion from the
// constant rather than from a hand-written list that could drift the same way.
let generated = desired_intercept_script("/tmp/x.db", "/tmp/mcp");
let repo = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../.claude/hooks/lumen_read_intercept.sh");

let mut copies = vec![("generated".to_string(), generated)];
if let Ok(body) = std::fs::read_to_string(&repo) {
copies.push(("repo".to_string(), body));
}

for (which, body) in &copies {
// The case arm itself, not the FILE_TYPE assignment: the generated copy puts both on
// one line while the repo copy splits them, so keying on the assignment finds the wrong
// line in one of them.
let arm = body
.lines()
.map(str::trim)
.find(|l| l.starts_with("log|"))
.unwrap_or_else(|| panic!("[{which}] no log case arm found in the intercept"));
for ext in lumen_core::coverage::LOG_EXTS {
// Split on the case-arm delimiters rather than substring-matching: `out|` is a
// substring of `output|`, so a plain `contains` would report `.out` as routed even
// if only `.output` were present.
let arm_exts: Vec<&str> = arm
.split(')')
.next()
.unwrap_or("")
.split('|')
.map(str::trim)
.collect();
assert!(
arm_exts.contains(ext),
"[{which}] the log arm does not route .{ext}: {arm}"
);
}
}
assert!(
copies.len() == 2 || std::env::var("CI").is_err(),
"the repo hook copy should be present in a source checkout"
);
}

#[test]
fn both_meter_hooks_agree_on_the_provenance_they_record() {
// Replaces a test that grepped both scripts for column *names*. That is why an exit-code
Expand Down
Loading