Merge pull request #18 from glitchySid/refactor

Refactor cache API and expand installation docs
This commit is contained in:
Siddhesh Mhatre
2026-01-10 22:32:03 +05:30
committed by GitHub
6 changed files with 344 additions and 211 deletions

View File

@@ -26,26 +26,53 @@ NoEntropy is a smart command-line tool that organizes your cluttered Downloads f
### Installation ### Installation
**Option 1: Download Pre-built Binary** ### Download Pre-built Binary
Download the binary for your operating system from [releases](https://github.com/glitchySid/noentropy/releases): Download the latest release for your operating system from [releases](https://github.com/glitchySid/noentropy/releases):
| OS | Download |
|----|----------|
| Linux x86_64 | `noentropy-x86_64-unknown-linux-gnu.tar.gz` |
| macOS x86_64 | `noentropy-x86_64-apple-darwin.tar.gz` |
| macOS arm64 | `noentropy-aarch64-apple-darwin.tar.gz` |
| Windows x86_64 | `noentropy-x86_64-pc-windows-msvc.zip` |
**Linux/macOS:**
```bash ```bash
# Linux/macOS: Give execute permissions # Download and extract
chmod +x noentropy curl -LO https://github.com/glitchySid/noentropy/releases/latest/download/noentropy-x86_64-unknown-linux-gnu.tar.gz
tar -xzf noentropy-x86_64-unknown-linux-gnu.tar.gz
# Run NoEntropy # Add to PATH (user-level)
./noentropy mkdir -p ~/.local/bin
mv noentropy ~/.local/bin/
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc # or ~/.zshrc
source ~/.bashrc
# Verify
noentropy --help
``` ```
**Option 2: Build from Source** **Windows:**
```powershell
# Download and extract
Invoke-WebRequest -Uri "https://github.com/glitchySid/noentropy/releases/latest/download/noentropy-x86_64-pc-windows-msvc.zip" -OutFile "noentropy.zip"
Expand-Archive -Path "noentropy.zip" -DestinationPath "noentropy"
# Add to PATH (User-level)
$env:PATH += ";$env:USERPROFILE\AppData\Local\NoEntropy"
# Or add via System Properties:
# Win + R → sysdm.cpl → Environment Variables → Edit PATH
```
See the [Installation Guide](docs/INSTALLATION.md) for detailed instructions.
### Build from Source
```bash ```bash
# Clone repository
git clone https://github.com/glitchySid/noentropy.git git clone https://github.com/glitchySid/noentropy.git
cd noentropy cd noentropy
# Build and run
cargo build --release cargo build --release
./target/release/noentropy ./target/release/noentropy
``` ```

View File

@@ -6,7 +6,6 @@ This guide covers different ways to install and set up NoEntropy on your system.
Before installing NoEntropy, ensure you have: Before installing NoEntropy, ensure you have:
- **Rust 2024 Edition** or later (if building from source)
- **Google Gemini API Key** - Get one at [https://ai.google.dev/](https://ai.google.dev/) - **Google Gemini API Key** - Get one at [https://ai.google.dev/](https://ai.google.dev/)
- A folder full of unorganized files to clean up! - A folder full of unorganized files to clean up!
@@ -14,49 +13,208 @@ Before installing NoEntropy, ensure you have:
The easiest way to get started is to download a pre-built binary for your operating system. The easiest way to get started is to download a pre-built binary for your operating system.
1. **Download Binary** ### Step 1: Download the Binary
Visit the releases page and download the binary for your operating system (Windows, Linux, or macOS): Visit the [releases page](https://github.com/glitchySid/noentropy/releases) and download the appropriate archive for your system:
| Operating System | Architecture | File to Download |
|------------------|--------------|------------------|
| Linux | x86_64 | `noentropy-x86_64-unknown-linux-gnu.tar.gz` |
| macOS | x86_64 (Intel) | `noentropy-x86_64-apple-darwin.tar.gz` |
| macOS | arm64 (Apple Silicon) | `noentropy-aarch64-apple-darwin.tar.gz` |
| Windows | x86_64 | `noentropy-x86_64-pc-windows-msvc.zip` |
Or download directly from the command line:
**Linux:**
```bash ```bash
https://github.com/glitchySid/noentropy/releases curl -LO https://github.com/glitchySid/noentropy/releases/latest/download/noentropy-x86_64-unknown-linux-gnu.tar.gz
``` ```
2. **Give Permission (Linux/macOS only)** **macOS (Intel):**
Make the binary executable:
```bash ```bash
chmod +x noentropy curl -LO https://github.com/glitchySid/noentropy/releases/latest/download/noentropy-x86_64-apple-darwin.tar.gz
``` ```
3. **Run NoEntropy** **macOS (Apple Silicon):**
```bash
curl -LO https://github.com/glitchySid/noentropy/releases/latest/download/noentropy-aarch64-apple-darwin.tar.gz
```
**Windows (PowerShell):**
```powershell
Invoke-WebRequest -Uri "https://github.com/glitchySid/noentropy/releases/latest/download/noentropy-x86_64-pc-windows-msvc.zip" -OutFile "noentropy.zip"
```
### Step 2: Extract the Archive
**Linux/macOS:**
```bash
tar -xzf noentropy-x86_64-unknown-linux-gnu.tar.gz
```
**Windows:**
Right-click the downloaded zip file and select "Extract All..." or use PowerShell:
```powershell
Expand-Archive -Path "noentropy.zip" -DestinationPath "noentropy"
```
### Step 3: Add to PATH
You need to add the folder containing `noentropy` to your system's PATH so you can run it from anywhere.
#### Linux/macOS
**Option A: User-level (recommended, no sudo required)**
```bash ```bash
./noentropy # Create local bin directory if it doesn't exist
mkdir -p ~/.local/bin
# Move the binary to a location in your PATH
mv noentropy ~/.local/bin/
# Add to PATH temporarily for this session
export PATH="$HOME/.local/bin:$PATH"
# Verify it works
noentropy --help
``` ```
To make this permanent, add this line to your shell configuration file:
**For bash (`~/.bashrc` or `~/.bash_profile`):**
```bash
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
```
**For zsh (`~/.zshrc`):**
```bash
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc
```
**Option B: System-wide (requires sudo)**
```bash
# Move to system bin (requires sudo on most systems)
sudo mv noentropy /usr/local/bin/
# Verify it works
noentropy --help
```
#### Windows
**Option A: User-level (recommended)**
1. Move the extracted `noentropy.exe` to a folder, for example:
```
C:\Users\<YourUsername>\AppData\Local\NoEntropy
```
2. Add to User PATH:
- Press `Win + R`, type `sysdm.cpl`, press Enter
- Click "Environment Variables"
- Under "User variables", select "Path", click "Edit"
- Click "New" and add:
```
C:\Users\<YourUsername>\AppData\Local\NoEntropy
```
- Click "OK" on all dialogs
3. **Alternative using PowerShell (Admin):**
```powershell
# Create installation directory
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\AppData\Local\NoEntropy"
# Move the binary
Move-Item -Path ".\noentropy.exe" -Destination "$env:USERPROFILE\AppData\Local\NoEntropy\"
# Add to PATH (User level)
$path = [Environment]::GetEnvironmentVariable("PATH", "User")
$newPath = "$path;$env:USERPROFILE\AppData\Local\NoEntropy"
[Environment]::SetEnvironmentVariable("PATH", $newPath, "User")
# Verify
noentropy --help
```
4. **Restart your terminal** or start a new Command Prompt/PowerShell window for the PATH changes to take effect.
**Option B: System-wide (requires Administrator)**
```powershell
# Run PowerShell as Administrator
Move-Item -Path ".\noentropy.exe" -Destination "C:\Program Files\NoEntropy\noentropy.exe"
# Add to system PATH
$path = [Environment]::GetEnvironmentVariable("PATH", "Machine")
$newPath = "$path;C:\Program Files\NoEntropy"
[Environment]::SetEnvironmentVariable("PATH", $newPath, "Machine")
# Verify
noentropy --help
```
### Step 4: Verify Installation
```bash
noentropy --help
```
You should see the help message with available options.
---
## Option 2: Build from Source ## Option 2: Build from Source
If you prefer to build from source or want the latest development version: If you prefer to build from source or want the latest development version:
1. **Clone the Repository** ### Prerequisites
- **Rust 2024 Edition** or later - Install from [rustup.rs](https://rustup.rs/)
- **Git** - For cloning the repository
### Step 1: Clone the Repository
```bash ```bash
git clone https://github.com/glitchySid/noentropy.git git clone https://github.com/glitchySid/noentropy.git
cd noentropy cd noentropy
``` ```
2. **Build the Application** ### Step 2: Build the Application
```bash ```bash
cargo build --release cargo build --release
``` ```
3. **Run the Application** The binary will be located at `target/release/noentropy`.
### Step 3: Install Globally (Optional)
**Linux/macOS:**
```bash ```bash
./target/release/noentropy # User-level installation
mkdir -p ~/.local/bin
cp target/release/noentropy ~/.local/bin/
noentropy --help
``` ```
**Windows:**
```powershell
# Create installation directory
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\AppData\Local\NoEntropy"
# Copy the binary
Copy-Item -Path ".\target\release\noentropy.exe" -Destination "$env:USERPROFILE\AppData\Local\NoEntropy\"
# Add to PATH (see Windows instructions above)
```
---
## First-Run Setup ## First-Run Setup
On first run, NoEntropy will guide you through an interactive setup process: On first run, NoEntropy will guide you through an interactive setup process:
@@ -69,19 +227,40 @@ NoEntropy provides an interactive setup if configuration is missing:
- **Missing download folder?** → You'll be prompted to specify it (with default suggestion) - **Missing download folder?** → You'll be prompted to specify it (with default suggestion)
- **Both missing?** → You'll be guided through complete setup - **Both missing?** → You'll be guided through complete setup
Configuration is automatically saved to `~/.config/noentropy/config.toml` after interactive setup. Configuration is automatically saved to:
| OS | Path |
|----|------|
| Linux/macOS | `~/.config/noentropy/config.toml` |
| Windows | `%APPDATA%\NoEntropy\config.toml` |
### Manual Configuration ### Manual Configuration
Alternatively, you can manually create the configuration file: Alternatively, you can manually create the configuration file:
**Linux/macOS:**
```bash ```bash
mkdir -p ~/.config/noentropy
cp config.example.toml ~/.config/noentropy/config.toml cp config.example.toml ~/.config/noentropy/config.toml
nano ~/.config/noentropy/config.toml nano ~/.config/noentropy/config.toml
``` ```
**Windows:**
```powershell
# Create config directory
New-Item -ItemType Directory -Force -Path "$env:APPDATA\NoEntropy"
# Copy example config
Copy-Item -Path ".\config.example.toml" -Destination "$env:APPDATA\NoEntropy\config.toml"
# Edit with Notepad
notepad "$env:APPDATA\NoEntropy\config.toml"
```
See the [Configuration Guide](CONFIGURATION.md) for detailed configuration options. See the [Configuration Guide](CONFIGURATION.md) for detailed configuration options.
---
## Getting Your Gemini API Key ## Getting Your Gemini API Key
1. Visit [Google AI Studio](https://ai.google.dev/) 1. Visit [Google AI Studio](https://ai.google.dev/)
@@ -89,16 +268,28 @@ See the [Configuration Guide](CONFIGURATION.md) for detailed configuration optio
3. Create a new API key 3. Create a new API key
4. Copy the key to your configuration file or enter it during interactive setup 4. Copy the key to your configuration file or enter it during interactive setup
---
## Verification ## Verification
To verify your installation works correctly: To verify your installation works correctly:
1. Run NoEntropy with the `--dry-run` flag:
```bash ```bash
./noentropy --dry-run noentropy --help
``` ```
2. You should see NoEntropy scan your downloads folder and display an organization plan without moving any files. If you see the help output, installation was successful!
To test file organization:
```bash
# Organize your downloads folder (or configured folder)
noentropy --dry-run
```
You should see NoEntropy scan your folder and display an organization plan without moving any files.
---
## Next Steps ## Next Steps
@@ -106,15 +297,19 @@ To verify your installation works correctly:
- [Usage Guide](USAGE.md) - Learn how to use NoEntropy effectively - [Usage Guide](USAGE.md) - Learn how to use NoEntropy effectively
- [How It Works](HOW_IT_WORKS.md) - Understand the organization process - [How It Works](HOW_IT_WORKS.md) - Understand the organization process
---
## Troubleshooting ## Troubleshooting
If you encounter issues during installation, check the [Troubleshooting Guide](TROUBLESHOOTING.md). If you encounter issues during installation, check the [Troubleshooting Guide](TROUBLESHOOTING.md).
Common installation issues: Common installation issues:
- **"noentropy: command not found"**: The folder is not in your PATH. Restart your terminal or run `source ~/.bashrc` (or `source ~/.zshrc`).
- **Permission denied (Linux/macOS)**: Make sure the binary has execute permissions: `chmod +x noentropy`
- **Windows PATH not updating**: Restart your terminal or computer after adding to PATH.
- **Rust not installed**: Install Rust from [rustup.rs](https://rustup.rs/) - **Rust not installed**: Install Rust from [rustup.rs](https://rustup.rs/)
- **Build errors**: Ensure you have the latest Rust toolchain: `rustup update` - **Build errors**: Ensure you have the latest Rust toolchain: `rustup update`
- **Permission denied**: Make sure the binary has execute permissions (Linux/macOS)
--- ---

View File

@@ -93,17 +93,12 @@ impl GeminiClient {
) -> Result<OrganizationPlan, GeminiError> { ) -> Result<OrganizationPlan, GeminiError> {
let url = self.build_url(); let url = self.build_url();
// Check cache and get pre-fetched metadata in one pass // Check cache first
let cache_result = match (cache.as_ref(), base_path) { if let Some(ref mut c) = cache
(Some(c), Some(bp)) => Some(c.check_cache(&filenames, bp)), && let Some(bp) = base_path
_ => None, && let Some(cached) = c.check_cache(&filenames, bp)
};
// Return cached response if valid
if let Some(ref result) = cache_result
&& let Some(ref cached_response) = result.cached_response
{ {
return Ok(cached_response.clone()); return Ok(cached);
} }
let prompt = PromptBuilder::new(&filenames).build_categorization_prompt(&self.categories); let prompt = PromptBuilder::new(&filenames).build_categorization_prompt(&self.categories);
@@ -112,9 +107,9 @@ impl GeminiClient {
let res = self.send_request_with_retry(&url, &request_body).await?; let res = self.send_request_with_retry(&url, &request_body).await?;
let plan = self.parse_categorization_response(res).await?; let plan = self.parse_categorization_response(res).await?;
// Cache response using pre-fetched metadata (no second metadata lookup) // Cache the response
if let (Some(cache), Some(result)) = (cache.as_mut(), cache_result) { if let (Some(cache), Some(base_path)) = (cache, base_path) {
cache.cache_response_with_metadata(&filenames, plan.clone(), result.file_metadata); cache.cache_response(&filenames, plan.clone(), base_path);
} }
Ok(plan) Ok(plan)

View File

@@ -6,12 +6,6 @@ use std::fs;
use std::path::Path; use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
/// Result of checking the cache - includes pre-fetched metadata to avoid double lookups
pub struct CacheCheckResult {
pub cached_response: Option<OrganizationPlan>,
pub file_metadata: HashMap<String, FileMetadata>,
}
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct Cache { pub struct Cache {
entries: HashMap<String, CacheEntry>, entries: HashMap<String, CacheEntry>,
@@ -37,7 +31,10 @@ impl Cache {
} }
pub fn load_or_create(cache_path: &Path) -> Self { pub fn load_or_create(cache_path: &Path) -> Self {
if cache_path.exists() { if !cache_path.exists() {
return Self::new();
}
match fs::read_to_string(cache_path) { match fs::read_to_string(cache_path) {
Ok(content) => match serde_json::from_str::<Cache>(&content) { Ok(content) => match serde_json::from_str::<Cache>(&content) {
Ok(cache) => { Ok(cache) => {
@@ -54,10 +51,6 @@ impl Cache {
Self::new() Self::new()
} }
} }
} else {
println!("Creating new cache file");
Self::new()
}
} }
pub fn save(&self, cache_path: &Path) -> Result<(), Box<dyn std::error::Error>> { pub fn save(&self, cache_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
@@ -70,32 +63,13 @@ impl Cache {
Ok(()) Ok(())
} }
/// Checks cache and returns pre-fetched metadata to avoid double lookups. pub fn check_cache(&self, filenames: &[String], base_path: &Path) -> Option<OrganizationPlan> {
/// The returned metadata can be passed to `cache_response_with_metadata` on cache miss. let cache_key = Self::generate_cache_key(filenames);
pub fn check_cache(&self, filenames: &[String], base_path: &Path) -> CacheCheckResult { let entry = self.entries.get(&cache_key)?;
// Fetch metadata once for all files
let file_metadata: HashMap<String, FileMetadata> = filenames
.iter()
.filter_map(|filename| {
let file_path = base_path.join(filename);
Self::get_file_metadata(&file_path)
.ok()
.map(|m| (filename.clone(), m))
})
.collect();
let cache_key = self.generate_cache_key(filenames);
let cached_response = self.entries.get(&cache_key).and_then(|entry| {
// Validate all files are unchanged using pre-fetched metadata
let all_unchanged = filenames.iter().all(|filename| { let all_unchanged = filenames.iter().all(|filename| {
match ( let file_path = base_path.join(filename);
file_metadata.get(filename), FileMetadata::from_path(&file_path).ok().as_ref() == entry.file_metadata.get(filename)
entry.file_metadata.get(filename),
) {
(Some(current), Some(cached)) => current == cached,
_ => false,
}
}); });
if all_unchanged { if all_unchanged {
@@ -104,73 +78,25 @@ impl Cache {
} else { } else {
None None
} }
});
CacheCheckResult {
cached_response,
file_metadata,
}
} }
/// Cache response using pre-fetched metadata (avoids double metadata lookup)
pub fn cache_response_with_metadata(
&mut self,
filenames: &[String],
response: OrganizationPlan,
file_metadata: HashMap<String, FileMetadata>,
) {
let cache_key = self.generate_cache_key(filenames);
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let entry = CacheEntry {
response,
timestamp,
file_metadata,
};
self.entries.insert(cache_key, entry);
if self.entries.len() > self.max_entries {
self.evict_oldest();
}
println!("Cached response for {} files", filenames.len());
}
/// Legacy method - checks cache for a response (fetches metadata internally)
#[deprecated(
note = "Use check_cache() + cache_response_with_metadata() to avoid double metadata lookups"
)]
pub fn get_cached_response(
&self,
filenames: &[String],
base_path: &Path,
) -> Option<OrganizationPlan> {
let result = self.check_cache(filenames, base_path);
result.cached_response
}
/// Legacy method - caches a response (fetches metadata internally)
#[deprecated(note = "Use cache_response_with_metadata() with pre-fetched metadata")]
pub fn cache_response( pub fn cache_response(
&mut self, &mut self,
filenames: &[String], filenames: &[String],
response: OrganizationPlan, response: OrganizationPlan,
base_path: &Path, base_path: &Path,
) { ) {
let cache_key = self.generate_cache_key(filenames); let cache_key = Self::generate_cache_key(filenames);
let mut file_metadata = HashMap::new();
for filename in filenames { let file_metadata: HashMap<String, FileMetadata> = filenames
.iter()
.filter_map(|filename| {
let file_path = base_path.join(filename); let file_path = base_path.join(filename);
if let Ok(metadata) = Self::get_file_metadata(&file_path) { FileMetadata::from_path(&file_path)
file_metadata.insert(filename.clone(), metadata); .ok()
} .map(|m| (filename.clone(), m))
} })
.collect();
let timestamp = SystemTime::now() let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
@@ -192,12 +118,12 @@ impl Cache {
println!("Cached response for {} files", filenames.len()); println!("Cached response for {} files", filenames.len());
} }
fn generate_cache_key(&self, filenames: &[String]) -> String { fn generate_cache_key(filenames: &[String]) -> String {
let mut sorted_filenames = filenames.to_vec();
sorted_filenames.sort();
let mut hasher = Hasher::new(); let mut hasher = Hasher::new();
for filename in &sorted_filenames { let mut sorted: Vec<_> = filenames.iter().collect();
sorted.sort();
for filename in sorted {
hasher.update(filename.as_bytes()); hasher.update(filename.as_bytes());
hasher.update(b"|"); hasher.update(b"|");
} }
@@ -205,16 +131,6 @@ impl Cache {
hasher.finalize().to_hex().to_string() hasher.finalize().to_hex().to_string()
} }
fn get_file_metadata(file_path: &Path) -> Result<FileMetadata, Box<dyn std::error::Error>> {
let metadata = fs::metadata(file_path)?;
let modified = metadata.modified()?.duration_since(UNIX_EPOCH)?.as_secs();
Ok(FileMetadata {
size: metadata.len(),
modified,
})
}
pub fn cleanup_old_entries(&mut self, max_age_seconds: u64) { pub fn cleanup_old_entries(&mut self, max_age_seconds: u64) {
let current_time = SystemTime::now() let current_time = SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
@@ -231,8 +147,8 @@ impl Cache {
println!("Cleaned up {} old cache entries", removed_count); println!("Cleaned up {} old cache entries", removed_count);
} }
if self.entries.len() > self.max_entries { while self.entries.len() > self.max_entries {
self.compact_cache(); self.evict_oldest();
} }
} }
@@ -248,9 +164,11 @@ impl Cache {
} }
} }
fn compact_cache(&mut self) { pub fn len(&self) -> usize {
while self.entries.len() > self.max_entries { self.entries.len()
self.evict_oldest(); }
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
} }
} }

View File

@@ -1,7 +1,7 @@
pub mod cache; pub mod cache;
pub mod undo_log; pub mod undo_log;
pub use cache::{Cache, CacheCheckResult}; pub use cache::Cache;
pub use undo_log::UndoLog; pub use undo_log::UndoLog;
#[cfg(test)] #[cfg(test)]

View File

@@ -53,18 +53,18 @@ fn test_cache_stores_and_retrieves_organization_plans() {
}], }],
}; };
// Check cache (will also fetch metadata) // Check cache (returns None on miss)
let check_result = cache.check_cache(&filenames, temp_dir.path()); let cached = cache.check_cache(&filenames, temp_dir.path());
assert!(check_result.cached_response.is_none()); assert!(cached.is_none());
// Store in cache with metadata // Store in cache
cache.cache_response_with_metadata(&filenames, plan.clone(), check_result.file_metadata); cache.cache_response(&filenames, plan.clone(), temp_dir.path());
// Retrieve from cache // Retrieve from cache
let check_result2 = cache.check_cache(&filenames, temp_dir.path()); let cached2 = cache.check_cache(&filenames, temp_dir.path());
assert!(check_result2.cached_response.is_some()); assert!(cached2.is_some());
let cached = check_result2.cached_response.unwrap(); let cached = cached2.unwrap();
assert_eq!(cached.files.len(), 1); assert_eq!(cached.files.len(), 1);
assert_eq!(cached.files[0].filename, "test.txt"); assert_eq!(cached.files[0].filename, "test.txt");
} }
@@ -84,8 +84,7 @@ fn test_cache_invalidates_on_file_modification() {
}; };
// Cache the response // Cache the response
let check_result = cache.check_cache(&filenames, temp_dir.path()); cache.cache_response(&filenames, plan, temp_dir.path());
cache.cache_response_with_metadata(&filenames, plan, check_result.file_metadata);
// Wait longer to ensure filesystem timestamp changes (at least 1 second for most filesystems) // Wait longer to ensure filesystem timestamp changes (at least 1 second for most filesystems)
std::thread::sleep(std::time::Duration::from_secs(2)); std::thread::sleep(std::time::Duration::from_secs(2));
@@ -101,14 +100,14 @@ fn test_cache_invalidates_on_file_modification() {
let _ = fs::metadata(temp_dir.path().join("test.txt")); let _ = fs::metadata(temp_dir.path().join("test.txt"));
// Cache should be invalidated due to modification time change // Cache should be invalidated due to modification time change
let check_result2 = cache.check_cache(&filenames, temp_dir.path()); let cached = cache.check_cache(&filenames, temp_dir.path());
// Note: Cache invalidation depends on file metadata (size/mtime) changing. // Note: Cache invalidation depends on file metadata (size/mtime) changing.
// If the filesystem has coarse timestamp granularity, this test may be flaky. // If the filesystem has coarse timestamp granularity, this test may be flaky.
// The important behavior is that the cache CAN detect file changes. // The important behavior is that the cache CAN detect file changes.
// For a more robust test, we check that the cache at least loads without error. // For a more robust test, we check that the cache at least loads without error.
// In production, files are typically modified minutes/hours apart. // In production, files are typically modified minutes/hours apart.
if check_result2.cached_response.is_some() { if cached.is_some() {
// If cache wasn't invalidated, it means the filesystem timestamp // If cache wasn't invalidated, it means the filesystem timestamp
// didn't change within our sleep window - this is acceptable // didn't change within our sleep window - this is acceptable
// as long as the mechanism works for real-world use cases // as long as the mechanism works for real-world use cases
@@ -151,12 +150,11 @@ fn test_cache_handles_multiple_files() {
], ],
}; };
let check_result = cache.check_cache(&filenames, temp_dir.path()); cache.cache_response(&filenames, plan.clone(), temp_dir.path());
cache.cache_response_with_metadata(&filenames, plan.clone(), check_result.file_metadata);
let check_result2 = cache.check_cache(&filenames, temp_dir.path()); let cached = cache.check_cache(&filenames, temp_dir.path());
assert!(check_result2.cached_response.is_some()); assert!(cached.is_some());
assert_eq!(check_result2.cached_response.unwrap().files.len(), 3); assert_eq!(cached.unwrap().files.len(), 3);
} }
#[test] #[test]