storkit: create 365_story_surface_api_rate_limit_warnings_in_chat
This commit is contained in:
@@ -1,218 +0,0 @@
|
||||
use crate::slog;
|
||||
use crate::state::SessionState;
|
||||
use ignore::WalkBuilder;
|
||||
use serde::Serialize;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Serialize, Debug, poem_openapi::Object)]
|
||||
pub struct SearchResult {
|
||||
pub path: String,
|
||||
pub matches: usize,
|
||||
}
|
||||
|
||||
fn get_project_root(state: &SessionState) -> Result<PathBuf, String> {
|
||||
state.get_project_root()
|
||||
}
|
||||
|
||||
pub async fn search_files(
|
||||
query: String,
|
||||
state: &SessionState,
|
||||
) -> Result<Vec<SearchResult>, String> {
|
||||
let root = get_project_root(state)?;
|
||||
search_files_impl(query, root).await
|
||||
}
|
||||
|
||||
pub async fn search_files_impl(query: String, root: PathBuf) -> Result<Vec<SearchResult>, String> {
|
||||
let root_clone = root.clone();
|
||||
|
||||
let results = tokio::task::spawn_blocking(move || {
|
||||
let mut matches = Vec::new();
|
||||
let walker = WalkBuilder::new(&root_clone).git_ignore(true).build();
|
||||
|
||||
for result in walker {
|
||||
match result {
|
||||
Ok(entry) => {
|
||||
if !entry.file_type().map(|ft| ft.is_file()).unwrap_or(false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let path = entry.path();
|
||||
if let Ok(content) = fs::read_to_string(path)
|
||||
&& content.contains(&query)
|
||||
{
|
||||
let relative = path
|
||||
.strip_prefix(&root_clone)
|
||||
.unwrap_or(path)
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
matches.push(SearchResult {
|
||||
path: relative,
|
||||
matches: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(err) => slog!("Error walking dir: {}", err),
|
||||
}
|
||||
}
|
||||
|
||||
matches
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("Search task failed: {e}"))?;
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn setup_project(files: &[(&str, &str)]) -> TempDir {
|
||||
let dir = TempDir::new().unwrap();
|
||||
for (path, content) in files {
|
||||
let full = dir.path().join(path);
|
||||
if let Some(parent) = full.parent() {
|
||||
fs::create_dir_all(parent).unwrap();
|
||||
}
|
||||
fs::write(full, content).unwrap();
|
||||
}
|
||||
dir
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn finds_files_matching_query() {
|
||||
let dir = setup_project(&[
|
||||
("hello.txt", "hello world"),
|
||||
("goodbye.txt", "goodbye world"),
|
||||
]);
|
||||
|
||||
let results = search_files_impl("hello".to_string(), dir.path().to_path_buf())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].path, "hello.txt");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn returns_empty_for_no_matches() {
|
||||
let dir = setup_project(&[("file.txt", "some content")]);
|
||||
|
||||
let results = search_files_impl("nonexistent".to_string(), dir.path().to_path_buf())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(results.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn searches_nested_directories() {
|
||||
let dir = setup_project(&[
|
||||
("top.txt", "needle"),
|
||||
("sub/deep.txt", "needle in haystack"),
|
||||
("sub/other.txt", "no match here"),
|
||||
]);
|
||||
|
||||
let results = search_files_impl("needle".to_string(), dir.path().to_path_buf())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(results.len(), 2);
|
||||
let paths: Vec<&str> = results.iter().map(|r| r.path.as_str()).collect();
|
||||
assert!(paths.contains(&"top.txt"));
|
||||
assert!(paths.contains(&"sub/deep.txt"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn skips_directories_only_matches_files() {
|
||||
let dir = setup_project(&[("sub/file.txt", "content")]);
|
||||
|
||||
let results = search_files_impl("content".to_string(), dir.path().to_path_buf())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].path, "sub/file.txt");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn respects_gitignore() {
|
||||
let dir = setup_project(&[
|
||||
(".gitignore", "ignored/\n"),
|
||||
("kept.txt", "search term"),
|
||||
("ignored/hidden.txt", "search term"),
|
||||
]);
|
||||
|
||||
// Initialize a git repo so .gitignore is respected
|
||||
std::process::Command::new("git")
|
||||
.args(["init"])
|
||||
.current_dir(dir.path())
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
let results = search_files_impl("search term".to_string(), dir.path().to_path_buf())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].path, "kept.txt");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn search_files_with_session_state() {
|
||||
let dir = setup_project(&[("found.txt", "target_text")]);
|
||||
let state = SessionState::default();
|
||||
*state.project_root.lock().unwrap() = Some(dir.path().to_path_buf());
|
||||
|
||||
let results = search_files("target_text".to_string(), &state).await.unwrap();
|
||||
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].path, "found.txt");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn search_files_errors_without_project_root() {
|
||||
let state = SessionState::default();
|
||||
|
||||
let result = search_files("query".to_string(), &state).await;
|
||||
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("No project"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_result_serializes_and_debugs() {
|
||||
let sr = SearchResult {
|
||||
path: "src/main.rs".to_string(),
|
||||
matches: 3,
|
||||
};
|
||||
let json = serde_json::to_string(&sr).unwrap();
|
||||
assert!(json.contains("src/main.rs"));
|
||||
assert!(json.contains("3"));
|
||||
|
||||
let debug = format!("{sr:?}");
|
||||
assert!(debug.contains("SearchResult"));
|
||||
assert!(debug.contains("src/main.rs"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn skips_binary_files() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
// Write a file with invalid UTF-8 bytes
|
||||
let binary_path = dir.path().join("binary.bin");
|
||||
fs::write(&binary_path, [0xFF, 0xFE, 0x00, 0x01]).unwrap();
|
||||
// Write a valid text file with the search term
|
||||
fs::write(dir.path().join("text.txt"), "findme").unwrap();
|
||||
|
||||
let results = search_files_impl("findme".to_string(), dir.path().to_path_buf())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].path, "text.txt");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user