Files
yvgude--lean-ctx/rust/tests/legacy_path_firewall.rs
T
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:35:30 +08:00

153 lines
6.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Legacy-path firewall — regression guard for the GH #434#436 class.
//!
//! Building the HOME-based legacy data dir `~/.lean-ctx` directly (e.g.
//! `home.join(".lean-ctx")`) bypasses the XDG resolver in `core::paths` /
//! `core::data_dir`. That is exactly the "split-brain" those issues fixed: files
//! keep landing in `~/.lean-ctx` even though the resolver has moved everything to
//! the typed `$XDG_*` dirs. New code MUST go through `data_dir()` / `state_dir()`
//! / `config_dir()` / `cache_dir()` instead.
//!
//! This test pins the CURRENT set of files that still construct a home-based
//! legacy path and fails if a NEW one appears, so the debt can only shrink. The
//! allowlist is deliberately exhaustive (not a "src/core only" rule) because the
//! real codebase still carries historical direct writers we want to track.
//!
//! Project-local `<project>/.lean-ctx` directories (the per-repo index, sibling
//! to `.git`) are a different, legitimate concept and are intentionally NOT
//! flagged — only joins rooted at the user HOME are matched.
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
/// Files allowed to construct a home-based `~/.lean-ctx` path today.
///
/// Group 1 — resolver / migrator / uninstaller: these MUST know the legacy
/// location by definition. Group 2 — historical direct readers/writers (tracked
/// debt): permitted for now, must not grow; migrate to typed `core::paths`
/// resolvers over time and delete the entry here when done.
const ALLOWLIST: &[&str] = &[
// Group 1: legitimate owners of the legacy path.
"core/data_dir.rs",
"core/paths.rs",
"core/xdg_migrate.rs",
"uninstall/agents.rs",
"uninstall/mod.rs",
"doctor/common.rs",
// Group 2: pre-existing direct home-writers/readers (tracked debt).
"report.rs",
"core/slo.rs",
"core/providers/config_provider/discovery.rs",
"cli/wrapped_publish.rs",
"cli/dispatch/analytics/gain.rs",
"tui/event_reader.rs",
"dashboard/routes/agents.rs",
"tools/ctx_provider.rs",
];
fn collect_rs_files(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect_rs_files(&path, out);
} else if path.extension().is_some_and(|x| x == "rs") {
out.push(path);
}
}
}
/// True when the file builds the HOME legacy dir `~/.lean-ctx` directly: some
/// `join(".lean-ctx…")` is rooted at a `home` token (`home.join(".lean-ctx")`,
/// `dirs::home_dir()…join(".lean-ctx/logs")`, `…join(".lean-ctx/agents/…")`).
///
/// Both the bare dir (`join(".lean-ctx")`) and any subpath join
/// (`join(".lean-ctx/logs")`, `join(".lean-ctx/agents/tasks.json")`) are
/// matched: the byte right after `.lean-ctx` must be `"` or `/`, so the
/// project markers `.lean-ctx.toml` / `.lean-ctx-id` (followed by `.` / `-`)
/// stay out of scope. This closes the slash-variant gap that previously let
/// `proxy_autostart` (`.lean-ctx/logs`) and `a2a/task` (`.lean-ctx/agents/…`)
/// slip past the firewall.
///
/// The scan spans lines on purpose — the join is frequently chained on its own
/// line below `dirs::home_dir()`, which a line-by-line check missed (it
/// under-counted real writers like `core::agents`). A bounded, char-boundary
/// safe look-back keeps project-local roots (`project_root`, `root`, `cwd`,
/// `dir`) — which carry no `home` token — out of scope by design.
fn builds_home_legacy_path(text: &str) -> bool {
const PREFIX: &str = r#"join(".lean-ctx"#;
const LOOKBACK: usize = 160;
let bytes = text.as_bytes();
let mut search_from = 0;
while let Some(rel) = text[search_from..].find(PREFIX) {
let idx = search_from + rel;
let after = idx + PREFIX.len();
// Legacy dir itself (`"`) or a subpath under it (`/`) — never the
// `.lean-ctx.toml` / `.lean-ctx-id` project markers.
if matches!(bytes.get(after), Some(b'"' | b'/')) {
let mut start = idx.saturating_sub(LOOKBACK);
while start > 0 && !text.is_char_boundary(start) {
start -= 1;
}
if text[start..idx].contains("home") {
return true;
}
}
search_from = after;
}
false
}
#[test]
fn no_new_home_legacy_path_construction() {
let src = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let mut files = Vec::new();
collect_rs_files(&src, &mut files);
assert!(!files.is_empty(), "no source files found under {src:?}");
let mut offenders = BTreeSet::new();
for file in &files {
let Ok(text) = std::fs::read_to_string(file) else {
continue;
};
if builds_home_legacy_path(&text) {
let rel = file
.strip_prefix(&src)
.unwrap()
.to_string_lossy()
.replace('\\', "/");
offenders.insert(rel);
}
}
let allow: BTreeSet<String> = ALLOWLIST.iter().map(|s| (*s).to_string()).collect();
let added: Vec<&String> = offenders.difference(&allow).collect();
assert!(
added.is_empty(),
"New home-based `~/.lean-ctx` path construction detected.\n\
Use core::paths (data_dir/state_dir/config_dir/cache_dir) instead, or — if\n\
this is genuinely a resolver/migrator/uninstaller — add the file to\n\
ALLOWLIST in this test with a reason.\nOffending file(s):\n {}",
added
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.join("\n ")
);
let removed: Vec<&String> = allow.difference(&offenders).collect();
assert!(
removed.is_empty(),
"ALLOWLIST has entries that no longer construct a home `~/.lean-ctx` path.\n\
A writer was migrated to typed dirs — remove it from ALLOWLIST to keep the\n\
firewall honest:\n {}",
removed
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.join("\n ")
);
}