diff --git a/src/telemetry_window_demo/cli.py b/src/telemetry_window_demo/cli.py index cd53f24..8c73a70 100644 --- a/src/telemetry_window_demo/cli.py +++ b/src/telemetry_window_demo/cli.py @@ -214,11 +214,11 @@ def plot_command(args: argparse.Namespace) -> None: print(f" - {plot_path.name}") -def run_ai_demo_command(args: argparse.Namespace) -> None: - from .ai_assisted_detection_demo import default_demo_root, run_demo - - demo_root = Path(args.demo_root).resolve() if args.demo_root else default_demo_root() - result = run_demo(demo_root=demo_root) +def run_ai_demo_command(args: argparse.Namespace) -> None: + from .ai_assisted_detection_demo import default_demo_root, run_demo + + demo_root = _demo_root_path(args.demo_root, default_demo_root()) + result = run_demo(demo_root=demo_root) print(f"[OK] Loaded {result['raw_event_count']} raw events") print(f"[OK] Normalized {result['normalized_event_count']} events") @@ -235,8 +235,8 @@ def run_ai_demo_command(args: argparse.Namespace) -> None: def run_rule_dedup_demo_command(args: argparse.Namespace) -> None: from .rule_evaluation_and_dedup_demo import default_demo_root, run_demo - demo_root = Path(args.demo_root).resolve() if args.demo_root else default_demo_root() - result = run_demo(demo_root=demo_root) + demo_root = _demo_root_path(args.demo_root, default_demo_root()) + result = run_demo(demo_root=demo_root) print(f"[OK] Loaded {result['raw_hit_count']} raw rule hits") print(f"[OK] Retained {result['retained_alert_count']} alerts") @@ -250,7 +250,7 @@ def run_rule_dedup_demo_command(args: argparse.Namespace) -> None: def run_config_change_demo_command(args: argparse.Namespace) -> None: from .config_change_investigation_demo import default_demo_root, run_demo - demo_root = Path(args.demo_root).resolve() if args.demo_root else default_demo_root() + demo_root = _demo_root_path(args.demo_root, default_demo_root()) result = run_demo(demo_root=demo_root) print(f"[OK] Loaded {result['change_event_count']} config changes") @@ -270,6 +270,15 @@ def _display_path(path: Path) -> str: return resolved.as_posix() +def _demo_root_path(value: str | None, default_root: Path) -> Path: + demo_root = Path(value).resolve() if value else default_root.resolve() + if not demo_root.exists(): + raise FileNotFoundError(f"Demo root not found: {demo_root}") + if not demo_root.is_dir(): + raise ValueError(f"Demo root path is not a directory: {demo_root}") + return demo_root + + def _validate_run_config(config: Mapping[str, Any]) -> dict[str, Any]: time_config = _optional_mapping(config.get("time", {}), "time") feature_config = _optional_mapping(config.get("features", {}), "features") diff --git a/tests/test_cli_errors.py b/tests/test_cli_errors.py index 7c41d29..18b3a3e 100644 --- a/tests/test_cli_errors.py +++ b/tests/test_cli_errors.py @@ -255,6 +255,37 @@ def test_main_reports_file_plot_output_dir_without_traceback(tmp_path, capsys) - assert "Traceback" not in stderr +@pytest.mark.parametrize( + "command", + ["run-ai-demo", "run-rule-dedup-demo", "run-config-change-demo"], +) +def test_main_reports_file_demo_root_without_traceback(command, tmp_path, capsys) -> None: + demo_root = tmp_path / "demo-root" + demo_root.write_text("not a directory\n", encoding="utf-8") + + with pytest.raises(SystemExit) as excinfo: + main([command, "--demo-root", str(demo_root)]) + + assert excinfo.value.code == 1 + stderr = capsys.readouterr().err + assert stderr.startswith("error: ") + assert "Demo root path is not a directory" in stderr + assert "Traceback" not in stderr + + +def test_main_reports_missing_demo_root_without_traceback(tmp_path, capsys) -> None: + demo_root = tmp_path / "missing-demo-root" + + with pytest.raises(SystemExit) as excinfo: + main(["run-ai-demo", "--demo-root", str(demo_root)]) + + assert excinfo.value.code == 1 + stderr = capsys.readouterr().err + assert stderr.startswith("error: ") + assert "Demo root not found" in stderr + assert "Traceback" not in stderr + + def test_main_reports_directory_plot_artifact_without_traceback(tmp_path, capsys) -> None: features_path = tmp_path / "features.csv" features_path.write_text(