fix: allow Grep to target a file
This commit is contained in:
@@ -280,6 +280,153 @@ mod tests {
|
|||||||
assert!(!grep.output.contains("c.txt"));
|
assert!(!grep.output.contains("c.txt"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grep_accepts_a_direct_file_without_searching_siblings() {
|
||||||
|
let temp = tempfile::tempdir().unwrap();
|
||||||
|
let selected = temp.path().join("selected.txt");
|
||||||
|
std::fs::write(&selected, "before\nneedle selected\nafter\n").unwrap();
|
||||||
|
std::fs::write(temp.path().join("sibling.txt"), "needle sibling\n").unwrap();
|
||||||
|
let root = temp.path().canonicalize().unwrap();
|
||||||
|
let readable = RootAccess(root.clone());
|
||||||
|
|
||||||
|
let direct = run_grep(
|
||||||
|
&root,
|
||||||
|
selected,
|
||||||
|
GrepRequest {
|
||||||
|
pattern: "needle".to_string(),
|
||||||
|
path: FsPath::new("selected.txt").unwrap(),
|
||||||
|
glob: None,
|
||||||
|
file_type: None,
|
||||||
|
case_insensitive: false,
|
||||||
|
before_context: 1,
|
||||||
|
after_context: 1,
|
||||||
|
multiline: false,
|
||||||
|
output_mode: GrepOutputMode::Content,
|
||||||
|
limit: 10,
|
||||||
|
offset: 0,
|
||||||
|
},
|
||||||
|
&readable,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(direct.match_count, 1);
|
||||||
|
assert_eq!(direct.matched_files, 1);
|
||||||
|
assert_eq!(
|
||||||
|
direct.output,
|
||||||
|
concat!(
|
||||||
|
"selected.txt\n",
|
||||||
|
" 1 │ before\n",
|
||||||
|
" > 2 │ needle selected\n",
|
||||||
|
" 3 │ after\n",
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert!(!direct.output.contains("sibling"));
|
||||||
|
|
||||||
|
let directory = run_grep(
|
||||||
|
&root,
|
||||||
|
root.clone(),
|
||||||
|
GrepRequest {
|
||||||
|
pattern: "needle".to_string(),
|
||||||
|
path: FsPath::root(),
|
||||||
|
glob: None,
|
||||||
|
file_type: None,
|
||||||
|
case_insensitive: false,
|
||||||
|
before_context: 0,
|
||||||
|
after_context: 0,
|
||||||
|
multiline: false,
|
||||||
|
output_mode: GrepOutputMode::Content,
|
||||||
|
limit: 10,
|
||||||
|
offset: 0,
|
||||||
|
},
|
||||||
|
&readable,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(directory.match_count, 2);
|
||||||
|
assert_eq!(directory.matched_files, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grep_returns_not_found_for_a_missing_direct_path() {
|
||||||
|
let temp = tempfile::tempdir().unwrap();
|
||||||
|
let root = temp.path().canonicalize().unwrap();
|
||||||
|
let missing = root.join("missing.txt");
|
||||||
|
let readable = RootAccess(root.clone());
|
||||||
|
|
||||||
|
let error = run_grep(
|
||||||
|
&root,
|
||||||
|
missing.clone(),
|
||||||
|
GrepRequest {
|
||||||
|
pattern: "needle".to_string(),
|
||||||
|
path: FsPath::new("missing.txt").unwrap(),
|
||||||
|
glob: None,
|
||||||
|
file_type: None,
|
||||||
|
case_insensitive: false,
|
||||||
|
before_context: 0,
|
||||||
|
after_context: 0,
|
||||||
|
multiline: false,
|
||||||
|
output_mode: GrepOutputMode::Content,
|
||||||
|
limit: 10,
|
||||||
|
offset: 0,
|
||||||
|
},
|
||||||
|
&readable,
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
|
||||||
|
assert!(matches!(error, FsError::NotFound(path) if path == missing));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn grep_keeps_direct_symlink_directory_and_broken_path_guards() {
|
||||||
|
use std::os::unix::fs::symlink;
|
||||||
|
|
||||||
|
let temp = tempfile::tempdir().unwrap();
|
||||||
|
let root = temp.path().canonicalize().unwrap();
|
||||||
|
let readable = RootAccess(root.clone());
|
||||||
|
std::fs::create_dir(root.join("target-dir")).unwrap();
|
||||||
|
symlink(root.join("target-dir"), root.join("directory-link")).unwrap();
|
||||||
|
symlink(root.join("missing-target"), root.join("broken-link")).unwrap();
|
||||||
|
|
||||||
|
let request = |path: &str| GrepRequest {
|
||||||
|
pattern: "needle".to_string(),
|
||||||
|
path: FsPath::new(path).unwrap(),
|
||||||
|
glob: None,
|
||||||
|
file_type: None,
|
||||||
|
case_insensitive: false,
|
||||||
|
before_context: 0,
|
||||||
|
after_context: 0,
|
||||||
|
multiline: false,
|
||||||
|
output_mode: GrepOutputMode::Content,
|
||||||
|
limit: 10,
|
||||||
|
offset: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let directory_error = run_grep(
|
||||||
|
&root,
|
||||||
|
root.join("directory-link"),
|
||||||
|
request("directory-link"),
|
||||||
|
&readable,
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(matches!(
|
||||||
|
directory_error,
|
||||||
|
FsError::SymlinkDirectoryNotTraversed { tool: "Grep", path, .. }
|
||||||
|
if path == root.join("directory-link")
|
||||||
|
));
|
||||||
|
|
||||||
|
let broken_error = run_grep(
|
||||||
|
&root,
|
||||||
|
root.join("broken-link"),
|
||||||
|
request("broken-link"),
|
||||||
|
&readable,
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(matches!(
|
||||||
|
broken_error,
|
||||||
|
FsError::BrokenSymlink { path, .. } if path == root.join("broken-link")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn grep_content_groups_lines_by_file_and_marks_matches() {
|
fn grep_content_groups_lines_by_file_and_marks_matches() {
|
||||||
let temp = tempfile::tempdir().unwrap();
|
let temp = tempfile::tempdir().unwrap();
|
||||||
|
|||||||
@@ -221,13 +221,15 @@ pub fn run_grep(
|
|||||||
std::io::ErrorKind::NotFound => FsError::NotFound(base.clone()),
|
std::io::ErrorKind::NotFound => FsError::NotFound(base.clone()),
|
||||||
_ => FsError::io(&base, e),
|
_ => FsError::io(&base, e),
|
||||||
})?;
|
})?;
|
||||||
if !base_meta.is_dir() {
|
if !base_meta.is_file() && !base_meta.is_dir() {
|
||||||
return Err(FsError::InvalidArgument(format!(
|
return Err(FsError::InvalidArgument(format!(
|
||||||
"grep search path is not a directory: {}",
|
"grep search path must be a regular file or directory: {}",
|
||||||
base.display()
|
base.display()
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
if let Some(info) = symlink.as_ref() {
|
if base_meta.is_dir()
|
||||||
|
&& let Some(info) = symlink.as_ref()
|
||||||
|
{
|
||||||
return Err(FsError::SymlinkDirectoryNotTraversed {
|
return Err(FsError::SymlinkDirectoryNotTraversed {
|
||||||
tool: "Grep",
|
tool: "Grep",
|
||||||
path: base.clone(),
|
path: base.clone(),
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ enum OutputMode {
|
|||||||
#[derive(Debug, Deserialize, JsonSchema)]
|
#[derive(Debug, Deserialize, JsonSchema)]
|
||||||
struct GrepParams {
|
struct GrepParams {
|
||||||
pattern: String,
|
pattern: String,
|
||||||
/// Logical Workdir-relative path to search. Defaults to the Workdir root.
|
/// Logical Workdir-relative file or directory to search. Defaults to the Workdir root.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
path: Option<String>,
|
path: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -129,7 +129,7 @@ pub fn grep_tool(session: WorkdirSessionHandle) -> ToolDefinition {
|
|||||||
Arc::new(move || {
|
Arc::new(move || {
|
||||||
let schema = schemars::schema_for!(GrepParams);
|
let schema = schemars::schema_for!(GrepParams);
|
||||||
let meta = ToolMeta::new("Grep")
|
let meta = ToolMeta::new("Grep")
|
||||||
.description("Search Workdir file contents with a regex. Content results group lines by file; `>` marks matching lines and unmarked lines are context. Glob/Grep traversal executes inside the WorkdirSession provider. Results are bounded and Workdir-relative.")
|
.description("Search a Workdir file or directory with a regex. Content results group lines by file; `>` marks matching lines and unmarked lines are context. Directory traversal executes inside the WorkdirSession provider. Results are bounded and Workdir-relative.")
|
||||||
.input_schema(serde_json::to_value(schema).expect("Grep schema serialization"));
|
.input_schema(serde_json::to_value(schema).expect("Grep schema serialization"));
|
||||||
let tool: Arc<dyn Tool> = Arc::new(GrepTool {
|
let tool: Arc<dyn Tool> = Arc::new(GrepTool {
|
||||||
session: session.clone(),
|
session: session.clone(),
|
||||||
|
|||||||
@@ -1943,7 +1943,7 @@ mod tests {
|
|||||||
&workdir,
|
&workdir,
|
||||||
GrepRequest {
|
GrepRequest {
|
||||||
pattern: "NEEDLE".into(),
|
pattern: "NEEDLE".into(),
|
||||||
path: WorkdirPath::root(),
|
path: WorkdirPath::new("src/main.rs").unwrap(),
|
||||||
glob: None,
|
glob: None,
|
||||||
file_type: None,
|
file_type: None,
|
||||||
case_insensitive: false,
|
case_insensitive: false,
|
||||||
|
|||||||
@@ -1886,8 +1886,8 @@ mod tests {
|
|||||||
use manifest::{Scope, SharedScope};
|
use manifest::{Scope, SharedScope};
|
||||||
use tower::ServiceExt;
|
use tower::ServiceExt;
|
||||||
use workdir::{
|
use workdir::{
|
||||||
LocalWorkdirSession, ReadRequest, StatRequest, Workdir, WorkdirPath,
|
GrepOutputMode, GrepRequest, LocalWorkdirSession, ReadRequest, StatRequest, Workdir,
|
||||||
WorkdirSessionCapabilities,
|
WorkdirPath, WorkdirSessionCapabilities,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn test_bundle(profile: ProfileSelector) -> ConfigBundle {
|
fn test_bundle(profile: ProfileSelector) -> ConfigBundle {
|
||||||
@@ -2348,6 +2348,40 @@ mod tests {
|
|||||||
.expect("owned operation");
|
.expect("owned operation");
|
||||||
assert!(matches!(result, WorkdirSessionOperationResult::Stat(_)));
|
assert!(matches!(result, WorkdirSessionOperationResult::Stat(_)));
|
||||||
|
|
||||||
|
let grep = WorkdirSessionOperationRequest {
|
||||||
|
delegations: Vec::new(),
|
||||||
|
operation: WorkdirSessionOperation::Grep(GrepRequest {
|
||||||
|
pattern: "hello".into(),
|
||||||
|
path: WorkdirPath::new("hello.txt").unwrap(),
|
||||||
|
glob: None,
|
||||||
|
file_type: None,
|
||||||
|
case_insensitive: false,
|
||||||
|
before_context: 0,
|
||||||
|
after_context: 0,
|
||||||
|
multiline: false,
|
||||||
|
output_mode: GrepOutputMode::Content,
|
||||||
|
limit: 10,
|
||||||
|
offset: 0,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
let Json(result) = run_workdir_session_operation(
|
||||||
|
State(state.clone()),
|
||||||
|
Path("session-1".to_string()),
|
||||||
|
Some(Extension(auth.clone())),
|
||||||
|
Ok(Json(grep)),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("grep direct file through provider operation");
|
||||||
|
match result {
|
||||||
|
WorkdirSessionOperationResult::Grep(result) => {
|
||||||
|
assert_eq!(result.match_count, 1);
|
||||||
|
assert_eq!(result.matched_files, 1);
|
||||||
|
assert!(result.output.starts_with("hello.txt\n"));
|
||||||
|
assert!(result.output.contains("> 1 │ hello"));
|
||||||
|
}
|
||||||
|
other => panic!("unexpected workdir grep result: {other:?}"),
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
let delegated_visible = WorkdirSessionOperationRequest {
|
let delegated_visible = WorkdirSessionOperationRequest {
|
||||||
|
|||||||
Reference in New Issue
Block a user