Skip to content

Commit 3bb0486

Browse files
committed
cowork-bot: scan command fails loudly when no config files load (silent-green guard)
- exit 1 if zero config files loaded across all environment dirs - exit 1 if baseline env loaded no keys (empty-baseline diff would flag everything) - +5 regression tests in TestScanEmptyGuards; suite 148 passed, ruff clean
1 parent 30db49b commit 3bb0486

2 files changed

Lines changed: 76 additions & 0 deletions

File tree

src/configdrift/cli.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,7 @@ def scan(
280280
raise typer.Exit(code=1)
281281

282282
env_configs: dict[str, dict[str, Any]] = {}
283+
files_loaded = 0
283284
for env_name, dir_path in dir_mapping.items():
284285
env_configs[env_name] = {}
285286
p = Path(dir_path)
@@ -294,9 +295,26 @@ def scan(
294295
try:
295296
data = load_file(str(f))
296297
env_configs[env_name].update(data)
298+
files_loaded += 1
297299
except Exception as e:
298300
console.print(f"[yellow]Warning: could not load {f}: {e}[/yellow]")
299301

302+
# Silent-failure guard: if nothing was actually loaded, any "no drift"
303+
# result would be a false green. Fail loudly instead.
304+
if files_loaded == 0:
305+
console.print(
306+
"[red]ERROR: No config files could be loaded from any environment "
307+
"directory. Refusing to report 'no drift' from an empty scan.[/red]"
308+
)
309+
raise typer.Exit(code=1)
310+
if not env_configs.get(baseline):
311+
console.print(
312+
f"[red]ERROR: Baseline environment '{baseline}' loaded no config "
313+
"keys; comparison against an empty baseline would flag every key "
314+
"as drift.[/red]"
315+
)
316+
raise typer.Exit(code=1)
317+
300318
results = diff_environments(env_configs, baseline_env=baseline)
301319

302320
if output == OutputFormat.JSON:

tests/test_cli.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,3 +473,61 @@ def test_version_output(self):
473473
assert result.exit_code == 0
474474
assert "configdrift" in result.stdout
475475
assert "0.1.0" in result.stdout
476+
477+
478+
class TestScanEmptyGuards:
479+
"""Regression: scan must not report a false-green 'no drift' when nothing loaded."""
480+
481+
def test_scan_all_dirs_missing_exits_1(self):
482+
with tempfile.TemporaryDirectory() as tmpdir:
483+
result = runner.invoke(
484+
app, ["scan", str(Path(tmpdir) / "nope1"), str(Path(tmpdir) / "nope2")]
485+
)
486+
assert result.exit_code == 1
487+
# Either the baseline-not-found guard or the empty-scan guard fires.
488+
assert (
489+
"not found" in result.stdout
490+
or "No config files could be loaded" in result.stdout
491+
)
492+
493+
def test_scan_empty_dirs_exits_1(self):
494+
with tempfile.TemporaryDirectory() as tmpdir:
495+
dev = Path(tmpdir) / "dev"
496+
prod = Path(tmpdir) / "prod"
497+
dev.mkdir()
498+
prod.mkdir()
499+
result = runner.invoke(app, ["scan", str(dev), str(prod)])
500+
assert result.exit_code == 1
501+
assert "No config files could be loaded" in result.stdout
502+
503+
def test_scan_baseline_dir_missing_exits_1(self):
504+
with tempfile.TemporaryDirectory() as tmpdir:
505+
prod = Path(tmpdir) / "prod"
506+
prod.mkdir()
507+
(prod / "config.yaml").write_text(yaml.dump({"host": "prod.example.com"}))
508+
result = runner.invoke(app, ["scan", str(prod), "--baseline", "dev"])
509+
assert result.exit_code == 1
510+
511+
def test_scan_baseline_loaded_nothing_exits_1(self):
512+
"""Baseline dir exists but only contains unparseable files -> refuse empty-baseline diff."""
513+
with tempfile.TemporaryDirectory() as tmpdir:
514+
dev = Path(tmpdir) / "dev"
515+
prod = Path(tmpdir) / "prod"
516+
dev.mkdir()
517+
prod.mkdir()
518+
(dev / "broken.yaml").write_text(":::: not yaml :::\n")
519+
(prod / "config.yaml").write_text(yaml.dump({"host": "prod.example.com"}))
520+
result = runner.invoke(app, ["scan", str(dev), str(prod)])
521+
assert result.exit_code == 1
522+
assert "loaded no config" in result.stdout
523+
524+
def test_scan_healthy_still_works(self):
525+
with tempfile.TemporaryDirectory() as tmpdir:
526+
dev = Path(tmpdir) / "dev"
527+
prod = Path(tmpdir) / "prod"
528+
dev.mkdir()
529+
prod.mkdir()
530+
(dev / "config.yaml").write_text(yaml.dump({"host": "localhost"}))
531+
(prod / "config.yaml").write_text(yaml.dump({"host": "prod.example.com"}))
532+
result = runner.invoke(app, ["scan", str(dev), str(prod)])
533+
assert result.exit_code == 0

0 commit comments

Comments
 (0)