@@ -332,6 +332,112 @@ def fake_run_scan_sync(
332332 assert output ["findings" ][0 ]["title" ] == "XSS"
333333
334334
335+ class TestNoFailFlag :
336+ def test_no_fail_exits_zero_with_findings (self , monkeypatch , tmp_path : Path ) -> None :
337+ """--no-fail should exit 0 even when findings are present."""
338+ from ai_sec_scan .models import Finding , Severity
339+
340+ finding = Finding (
341+ file_path = "app.py" ,
342+ line_start = 10 ,
343+ severity = Severity .HIGH ,
344+ title = "Hardcoded Secret" ,
345+ description = "API key in source" ,
346+ recommendation = "Use environment variables" ,
347+ )
348+
349+ def fake_get_provider (provider_name : str , model : str | None ) -> BaseProvider :
350+ return _DummyProvider ()
351+
352+ def fake_run_scan_sync (
353+ path ,
354+ provider ,
355+ include = None ,
356+ exclude = None ,
357+ max_file_size_kb = 100 ,
358+ min_severity = None ,
359+ quiet = False ,
360+ cache_dir = None ,
361+ no_cache = False ,
362+ parallel = 1 ,
363+ ):
364+ return ScanResult (
365+ findings = [finding ],
366+ files_scanned = 1 ,
367+ scan_duration = 0.01 ,
368+ provider = provider .name ,
369+ model = provider .model ,
370+ )
371+
372+ monkeypatch .setattr ("ai_sec_scan.cli._get_provider" , fake_get_provider )
373+ monkeypatch .setattr ("ai_sec_scan.scanner.run_scan_sync" , fake_run_scan_sync )
374+
375+ runner = CliRunner ()
376+ with runner .isolated_filesystem ():
377+ project_dir = Path ("project" )
378+ project_dir .mkdir ()
379+ (project_dir / "app.py" ).write_text ("print('ok')" , encoding = "utf-8" )
380+
381+ result = runner .invoke (
382+ main ,
383+ ["scan" , "project" , "-o" , "json" , "-q" , "--no-fail" ],
384+ )
385+
386+ assert result .exit_code == 0
387+
388+ def test_without_no_fail_exits_nonzero (self , monkeypatch , tmp_path : Path ) -> None :
389+ """Without --no-fail, findings should produce exit code 1."""
390+ from ai_sec_scan .models import Finding , Severity
391+
392+ finding = Finding (
393+ file_path = "app.py" ,
394+ line_start = 10 ,
395+ severity = Severity .LOW ,
396+ title = "Debug Mode Enabled" ,
397+ description = "debug=True in production" ,
398+ recommendation = "Disable debug mode" ,
399+ )
400+
401+ def fake_get_provider (provider_name : str , model : str | None ) -> BaseProvider :
402+ return _DummyProvider ()
403+
404+ def fake_run_scan_sync (
405+ path ,
406+ provider ,
407+ include = None ,
408+ exclude = None ,
409+ max_file_size_kb = 100 ,
410+ min_severity = None ,
411+ quiet = False ,
412+ cache_dir = None ,
413+ no_cache = False ,
414+ parallel = 1 ,
415+ ):
416+ return ScanResult (
417+ findings = [finding ],
418+ files_scanned = 1 ,
419+ scan_duration = 0.01 ,
420+ provider = provider .name ,
421+ model = provider .model ,
422+ )
423+
424+ monkeypatch .setattr ("ai_sec_scan.cli._get_provider" , fake_get_provider )
425+ monkeypatch .setattr ("ai_sec_scan.scanner.run_scan_sync" , fake_run_scan_sync )
426+
427+ runner = CliRunner ()
428+ with runner .isolated_filesystem ():
429+ project_dir = Path ("project" )
430+ project_dir .mkdir ()
431+ (project_dir / "app.py" ).write_text ("print('ok')" , encoding = "utf-8" )
432+
433+ result = runner .invoke (
434+ main ,
435+ ["scan" , "project" , "-o" , "json" , "-q" ],
436+ )
437+
438+ assert result .exit_code == 1
439+
440+
335441class TestCacheStatsCommand :
336442 def test_stats_empty_cache (self , tmp_path : Path ) -> None :
337443 cache_dir = tmp_path / "cache"
0 commit comments