Skip to content

Commit c2cb757

Browse files
committed
fix(cli): bare --force refuses when no configured root exists
- the pre-clear gate covers both destructive input shapes: named CLI paths must all exist, and the bare lane refuses when a persisted index exists and not one configured root does — each missing root named, then 'Error: --force would clear the index with nothing to rebuild', exit 1, index untouched - partial-missing configured roots keep the documented partial-force semantics; fresh workspaces with no persisted index pass ungated - deliberate wipe stays rm -rf .codanna/index; --force is a rebuild, not a wipe
1 parent 5438ee4 commit c2cb757

2 files changed

Lines changed: 94 additions & 10 deletions

File tree

‎src/main.rs‎

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -414,17 +414,30 @@ async fn main() {
414414
let index_preexisted = persistence.exists();
415415

416416
// The force and emission-heal lanes clear the persisted index during
417-
// facade creation, before CLI paths reach validation downstream; a
418-
// mistyped path would destroy the index and rebuild nothing.
419-
// Existence is checked here, ahead of any destructive clear.
417+
// facade creation, before the rebuild sources are validated; a
418+
// mistyped CLI path — or a configured root set with no surviving
419+
// entry on the bare lane — would destroy the index and rebuild
420+
// nothing. Existence is checked here, ahead of any destructive clear.
420421
if let Commands::Index { paths, force, .. } = &cli.command {
421-
if (*force || emission_heal) && !paths.is_empty() {
422-
let mut missing = false;
423-
for path in paths.iter().filter(|p| !p.exists()) {
424-
missing = true;
425-
eprintln!("Error: Path does not exist: {}", path.display());
426-
}
427-
if missing {
422+
if *force || emission_heal {
423+
if !paths.is_empty() {
424+
let mut missing = false;
425+
for path in paths.iter().filter(|p| !p.exists()) {
426+
missing = true;
427+
eprintln!("Error: Path does not exist: {}", path.display());
428+
}
429+
if missing {
430+
std::process::exit(1);
431+
}
432+
} else if index_preexisted && !config.indexing.indexed_paths.iter().any(|p| p.exists())
433+
{
434+
for path in &config.indexing.indexed_paths {
435+
eprintln!(
436+
"Error: Configured path does not exist: {}",
437+
codanna::parsing::paths::render_absolute_path(path).display()
438+
);
439+
}
440+
eprintln!("Error: --force would clear the index with nothing to rebuild");
428441
std::process::exit(1);
429442
}
430443
}

‎tests/cli/test_index_force_invalid_path.rs‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,77 @@ fn force_with_missing_path_refuses_before_clearing_index() {
155155
);
156156
}
157157

158+
#[test]
159+
fn force_bare_with_no_existing_roots_refuses_before_clearing_index() {
160+
let temp = TempDir::new().expect("temp workspace");
161+
let workspace = temp.path();
162+
write_fixture(workspace);
163+
write_settings(workspace);
164+
165+
let (exit, stdout, stderr) = run_cli(workspace, &["index", "src"]);
166+
assert_eq!(
167+
exit, 0,
168+
"seed must succeed\nstdout:{stdout}\nstderr:{stderr}"
169+
);
170+
171+
let meta = workspace.join(".codanna/index/index.meta");
172+
assert!(meta.exists(), "seed must persist index.meta");
173+
let meta_before = std::fs::read(&meta).expect("read index.meta");
174+
let entries_before = index_dir_entries(workspace);
175+
176+
std::fs::remove_dir_all(workspace.join("src")).expect("remove registered root");
177+
178+
let (exit, stdout, stderr) = run_cli(workspace, &["index", "--force"]);
179+
assert_ne!(
180+
exit, 0,
181+
"bare force with no existing configured root must fail\nstdout:{stdout}\nstderr:{stderr}"
182+
);
183+
assert!(
184+
stderr.contains("Configured path does not exist"),
185+
"refusal must name the missing configured root:\nstderr:{stderr}"
186+
);
187+
assert!(
188+
stderr.contains("nothing to rebuild"),
189+
"refusal must state the reason:\nstderr:{stderr}"
190+
);
191+
192+
let meta_after = std::fs::read(&meta).expect("read index.meta");
193+
assert_eq!(
194+
meta_before, meta_after,
195+
"index.meta must be byte-identical after the refused bare-force run"
196+
);
197+
assert_eq!(
198+
entries_before,
199+
index_dir_entries(workspace),
200+
"index directory contents must be untouched after the refused bare-force run"
201+
);
202+
}
203+
204+
#[test]
205+
fn force_bare_with_existing_root_still_rebuilds() {
206+
let temp = TempDir::new().expect("temp workspace");
207+
let workspace = temp.path();
208+
write_fixture(workspace);
209+
write_settings(workspace);
210+
211+
let (exit, stdout, stderr) = run_cli(workspace, &["index", "src"]);
212+
assert_eq!(
213+
exit, 0,
214+
"seed must succeed\nstdout:{stdout}\nstderr:{stderr}"
215+
);
216+
217+
let (exit, stdout, stderr) = run_cli(workspace, &["index", "--force"]);
218+
assert_eq!(
219+
exit, 0,
220+
"bare force with an existing configured root must rebuild\nstdout:{stdout}\nstderr:{stderr}"
221+
);
222+
let combined = format!("{stdout}{stderr}");
223+
assert!(
224+
combined.contains("Index saved to"),
225+
"bare force rebuild must index and save:\nstdout:{stdout}\nstderr:{stderr}"
226+
);
227+
}
228+
158229
#[test]
159230
fn force_with_existing_path_still_rebuilds() {
160231
let temp = TempDir::new().expect("temp workspace");

0 commit comments

Comments
 (0)