Compare commits

...

3 Commits

Author SHA1 Message Date
Test
043766f86e fix: include non-.md files in vault cache incremental updates
The cache's git helper functions (collect_md_paths_from_diff,
collect_md_paths_from_porcelain, git_uncommitted_files) filtered for
.md files only. This meant .yml view files created by save_view were
never detected by the incremental cache update, so they didn't appear
in vault entries until a full rescan.

Renamed the helpers to collect_paths_from_diff/collect_paths_from_porcelain
and replaced the .md filter with a hidden-segment filter, matching the
behavior of scan_all_files which already includes all non-hidden files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 23:12:33 +02:00
Test
09e8d03851 test: update Sidebar test to match new behavior — types with 0 notes shown
The buildDynamicSections now includes type definitions with 0 instances.
Update the 'does not show section for type with zero active entries' test
to assert the new intended behavior: types with 0 notes appear in the
sidebar as long as their Type definition entry exists (not trashed/archived).
2026-04-04 23:08:39 +02:00
Test
9fa1c52a96 fix: wrap SearchPanel Enter keyDown in act() to prevent flaky test
The 'selects result on Enter' test could fail intermittently because
fireEvent.keyDown(window) fired before the useEffect re-registered
the keyboard handler after results loaded. Wrapping in act() ensures
effects are flushed before the event dispatches.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 22:25:55 +02:00
3 changed files with 81 additions and 18 deletions

View File

@@ -81,29 +81,38 @@ fn parse_porcelain_line(line: &str) -> Option<(&str, String)> {
Some((&line[..2], line[3..].trim().to_string()))
}
/// Extract .md file paths from git diff --name-only output.
fn collect_md_paths_from_diff(stdout: &str) -> Vec<String> {
/// Extract file paths from git diff --name-only output.
/// Includes all non-hidden files (not just .md) so the cache picks up
/// view files (.yml), binary assets, etc.
fn collect_paths_from_diff(stdout: &str) -> Vec<String> {
stdout
.lines()
.filter(|line| !line.is_empty() && line.ends_with(".md"))
.filter(|line| !line.is_empty() && !has_hidden_segment(line))
.map(|line| line.to_string())
.collect()
}
/// Extract .md file paths from git status --porcelain output.
fn collect_md_paths_from_porcelain(stdout: &str) -> Vec<String> {
/// Extract file paths from git status --porcelain output.
/// Includes all non-hidden files so incremental cache updates cover
/// every file type the vault scanner recognises.
fn collect_paths_from_porcelain(stdout: &str) -> Vec<String> {
stdout
.lines()
.filter_map(parse_porcelain_line)
.filter(|(_, path)| path.ends_with(".md"))
.filter(|(_, path)| !has_hidden_segment(path))
.map(|(_, path)| path)
.collect()
}
/// Return true if any path segment starts with `.` (hidden file/directory).
fn has_hidden_segment(path: &str) -> bool {
path.split('/').any(|seg| seg.starts_with('.'))
}
fn git_changed_files(vault: &Path, from_hash: &str, to_hash: &str) -> Vec<String> {
let diff_arg = format!("{}..{}", from_hash, to_hash);
let mut files = run_git(vault, &["diff", &diff_arg, "--name-only"])
.map(|s| collect_md_paths_from_diff(&s))
.map(|s| collect_paths_from_diff(&s))
.unwrap_or_default();
// Include uncommitted changes (modified, staged, and untracked files).
@@ -121,16 +130,16 @@ fn git_changed_files(vault: &Path, from_hash: &str, to_hash: &str) -> Vec<String
fn git_uncommitted_files(vault: &Path) -> Vec<String> {
// Modified/staged tracked files from git status --porcelain
let mut files: Vec<String> = run_git(vault, &["status", "--porcelain"])
.map(|s| collect_md_paths_from_porcelain(&s))
.map(|s| collect_paths_from_porcelain(&s))
.unwrap_or_default();
// Untracked files via ls-files (lists individual files, not just directories).
// git status --porcelain shows `?? dir/` for new directories, hiding individual
// files inside — ls-files resolves them so the cache picks up all new .md files.
// files inside — ls-files resolves them so the cache picks up all new files.
let untracked = run_git(vault, &["ls-files", "--others", "--exclude-standard"])
.map(|s| {
s.lines()
.filter(|l| !l.is_empty() && l.ends_with(".md"))
.filter(|l| !l.is_empty() && !has_hidden_segment(l))
.map(|l| l.to_string())
.collect::<Vec<_>>()
})
@@ -978,4 +987,56 @@ mod tests {
"stale cache with old version must be invalidated, re-parsing 'Archived: Yes' as true"
);
}
#[test]
fn test_update_same_commit_picks_up_new_yml_file() {
let (_lock, _cache_tmp, dir) = setup_git_vault();
let vault = dir.path();
create_test_file(vault, "note.md", "# Note\n\nContent.");
git_add_commit(vault, "init");
// Prime cache
let entries = scan_vault_cached(vault).unwrap();
assert_eq!(entries.len(), 1);
// Create a new .yml view file (untracked, like save_view does)
create_test_file(vault, "views/my-view.yml", "name: My View\nfilters: []\n");
// Same commit — new .yml file must appear in entries
let entries2 = scan_vault_cached(vault).unwrap();
assert!(
entries2.len() >= 2,
"new .yml file must be picked up by cache update, got {} entries",
entries2.len()
);
assert!(
entries2.iter().any(|e| e.path.contains("my-view.yml")),
"entries must include the new .yml file"
);
}
#[test]
fn test_incremental_different_commit_picks_up_yml_file() {
let (_lock, _cache_tmp, dir) = setup_git_vault();
let vault = dir.path();
create_test_file(vault, "note.md", "# Note\n\nContent.");
git_add_commit(vault, "init");
// Prime cache
let entries = scan_vault_cached(vault).unwrap();
assert_eq!(entries.len(), 1);
// Add a .yml file and commit
create_test_file(vault, "views/my-view.yml", "name: My View\nfilters: []\n");
git_add_commit(vault, "add view");
// Different commit — .yml file must appear in entries
let entries2 = scan_vault_cached(vault).unwrap();
assert!(
entries2.iter().any(|e| e.path.contains("my-view.yml")),
"committed .yml file must be picked up by incremental cache update"
);
}
}

View File

@@ -1,4 +1,4 @@
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { SearchPanel } from './SearchPanel'
import type { VaultEntry } from '../types'
@@ -239,12 +239,12 @@ describe('SearchPanel', () => {
expect(screen.getByText('How to Design AI-first APIs')).toBeInTheDocument()
})
fireEvent.keyDown(window, { key: 'Enter' })
await waitFor(() => {
expect(onSelectNote).toHaveBeenCalledWith(MOCK_ENTRIES[0])
expect(onClose).toHaveBeenCalled()
await act(async () => {
fireEvent.keyDown(window, { key: 'Enter' })
})
expect(onSelectNote).toHaveBeenCalledWith(MOCK_ENTRIES[0])
expect(onClose).toHaveBeenCalled()
})
it('shows result count and elapsed time', async () => {

View File

@@ -439,11 +439,13 @@ describe('Sidebar', () => {
expect(screen.queryByText('Pasta Carbonara')).not.toBeInTheDocument()
})
it('does not show section for type with zero active entries', () => {
it('shows section for type with zero active entries when type definition exists', () => {
// Only Type definitions exist for Book, no actual Book instances
// New behavior: types are shown in sidebar as long as the Type definition exists (not trashed/archived)
const entriesNoBookInstance = entriesWithCustomTypes.filter((e) => !(e.isA === 'Book' && e.title !== 'Book'))
render(<Sidebar entries={entriesNoBookInstance} selection={defaultSelection} onSelect={() => {}} />)
expect(screen.queryByText('Books')).not.toBeInTheDocument()
// Books should still appear because the Book type definition exists
expect(screen.getByText('Books')).toBeInTheDocument()
// Recipes still has an instance (Pasta Carbonara)
expect(screen.getByText('Recipes')).toBeInTheDocument()
})