diff --git a/src/serve_session.rs b/src/serve_session.rs index 7a0dad11..1a53f5b6 100644 --- a/src/serve_session.rs +++ b/src/serve_session.rs @@ -402,8 +402,16 @@ impl ServeSession { .filter_entry(&passes_ignore) .flatten() { - let _ = vfs.watch(entry.path()); - watch_count += 1; + // On macOS kqueue, non-recursive directory watches only + // fire for structural changes (child added/removed), NOT + // for child file content modifications. Individual file + // watches are required to detect edits. On Linux/Windows, + // directory watches already cover child file events so + // file watches would be redundant. + if entry.file_type().is_dir() || cfg!(target_os = "macos") { + let _ = vfs.watch(entry.path()); + watch_count += 1; + } } } // Watch project folder non-recursively for project file edits. diff --git a/src/snapshot_middleware/dir.rs b/src/snapshot_middleware/dir.rs index 0ed2e106..32e49188 100644 --- a/src/snapshot_middleware/dir.rs +++ b/src/snapshot_middleware/dir.rs @@ -49,11 +49,14 @@ pub fn snapshot_dir_no_meta( .all(|rule| rule.passes(child.path())) }; + let mut any_child_glob_ignored = false; + let snapshot_children = if std::env::var("ATLAS_SEQUENTIAL").is_ok() { let mut children = Vec::new(); for entry in vfs.read_dir(path)? { let entry = entry?; if !passes_filter_rules(&entry) { + any_child_glob_ignored = true; continue; } if let Some(child_snapshot) = snapshot_from_vfs(context, vfs, entry.path())? { @@ -66,6 +69,8 @@ pub fn snapshot_dir_no_meta( let entries: Vec<_> = vfs.read_dir(path)?.filter_map(|e| e.ok()).collect(); + any_child_glob_ignored = entries.iter().any(|e| !passes_filter_rules(e)); + let results: Vec>> = entries .par_iter() .map(|entry| { @@ -112,7 +117,8 @@ pub fn snapshot_dir_no_meta( InstanceMetadata::new() .instigating_source(path) .relevant_paths(relevant_paths) - .context(context), + .context(context) + .ignore_unknown_instances(any_child_glob_ignored), ); Ok(Some(snapshot)) @@ -564,4 +570,72 @@ mod test { insta::assert_yaml_snapshot!(instance_snapshot); } + + #[test] + fn glob_ignored_children_set_ignore_unknown_instances() { + use crate::glob::Glob; + use crate::snapshot::PathIgnoreRule; + use std::path::PathBuf; + + let mut imfs = InMemoryFs::new(); + imfs.load_snapshot( + "/project/src", + VfsSnapshot::dir([ + ("kept.luau", VfsSnapshot::file("return 1")), + ("ignored.luau", VfsSnapshot::file("return 2")), + ]), + ) + .unwrap(); + let vfs = Vfs::new(imfs); + + let mut context = InstanceContext::default(); + context.add_path_ignore_rules(vec![PathIgnoreRule { + base_path: PathBuf::from("/project"), + glob: Glob::new("src/ignored.luau").unwrap(), + }]); + + let snap = snapshot_dir(&context, &vfs, Path::new("/project/src"), "src") + .unwrap() + .unwrap(); + + assert!( + snap.metadata.ignore_unknown_instances, + "Directory with glob-ignored children should have ignore_unknown_instances = true" + ); + assert_eq!( + snap.children.len(), + 1, + "Only non-ignored child should be snapshotted" + ); + assert_eq!(snap.children[0].name, "kept"); + } + + #[test] + fn no_glob_ignored_children_leaves_ignore_unknown_instances_false() { + let mut imfs = InMemoryFs::new(); + imfs.load_snapshot( + "/project/src", + VfsSnapshot::dir([ + ("a.luau", VfsSnapshot::file("return 1")), + ("b.luau", VfsSnapshot::file("return 2")), + ]), + ) + .unwrap(); + let vfs = Vfs::new(imfs); + + let snap = snapshot_dir( + &InstanceContext::default(), + &vfs, + Path::new("/project/src"), + "src", + ) + .unwrap() + .unwrap(); + + assert!( + !snap.metadata.ignore_unknown_instances, + "Directory without glob-ignored children should have ignore_unknown_instances = false" + ); + assert_eq!(snap.children.len(), 2); + } }