"""Tests for CLI target-list argument parsing.""" from __future__ import annotations import importlib import json import sys from types import SimpleNamespace from typing import TYPE_CHECKING, Any import pytest if TYPE_CHECKING: from pathlib import Path cli_main: Any = importlib.import_module("strix.interface.main") def _stub_settings(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr( cli_main, "load_settings", lambda: SimpleNamespace(runtime=SimpleNamespace(max_local_copy_mb=1024)), ) def test_parse_arguments_accepts_target_list_file( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: target_list = tmp_path / "targets.txt" target_list.write_text( "https://test1.com/\n\nhttp://test2.com:5789/\n", encoding="utf-8", ) _stub_settings(monkeypatch) monkeypatch.setattr(sys, "argv", ["strix", "--target-list", str(target_list), "-n"]) args = cli_main.parse_arguments() assert [target["original"] for target in args.targets_info] == [ "https://test1.com/", "http://test2.com:5789/", ] assert [target["type"] for target in args.targets_info] == [ "web_application", "web_application", ] def test_parse_arguments_combines_target_and_target_list( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: target_list = tmp_path / "targets.txt" target_list.write_text("http://test2.com:5789/\n", encoding="utf-8") _stub_settings(monkeypatch) monkeypatch.setattr( sys, "argv", ["strix", "-t", "https://test1.com/", "--target-list", str(target_list)], ) args = cli_main.parse_arguments() assert [target["original"] for target in args.targets_info] == [ "https://test1.com/", "http://test2.com:5789/", ] def test_parse_arguments_rejects_resume_with_target_list( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: target_list = tmp_path / "targets.txt" target_list.write_text("https://test1.com/\n", encoding="utf-8") monkeypatch.setattr( sys, "argv", ["strix", "--resume", "old-run", "--target-list", str(target_list)], ) with pytest.raises(SystemExit): cli_main.parse_arguments() assert "Cannot combine --resume with --target/--target-list" in capsys.readouterr().err def _write_run_record(runs_dir: Path, run_name: str, record: dict[str, Any]) -> None: """Write a resumable run: its record plus the agent snapshot resume needs.""" run_dir = runs_dir / run_name run_dir.mkdir(parents=True, exist_ok=True) (run_dir / "run.json").write_text(json.dumps(record), encoding="utf-8") state_dir = run_dir / ".state" state_dir.mkdir(exist_ok=True) (state_dir / "agents.json").write_text("{}", encoding="utf-8") def test_resume_restores_a_target_less_workspace_mount( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """A run that only mounted a working directory is resumable.""" work = tmp_path / "project" work.mkdir() monkeypatch.chdir(tmp_path) _write_run_record( tmp_path / "strix_runs", "pentest_abcd", { "run_name": "pentest_abcd", "targets_info": [], "local_sources": [], "workspace_mount": str(work), "instruction": "audit the auth flow", "scan_mode": "deep", }, ) monkeypatch.setattr(sys, "argv", ["strix", "--resume", "pentest_abcd"]) args = cli_main.parse_arguments() # Still genuinely target-less, and the workspace is mounted again. assert args.targets_info == [] assert args.workspace_mount == str(work) assert args.local_sources == [ {"source_path": str(work), "workspace_subdir": "project", "protect_metadata": True} ] assert args.instruction == "audit the auth flow" def test_resume_revalidates_persisted_workspace_files( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """Resume places the same files again, and drops ones that went away.""" work = tmp_path / "project" work.mkdir() kept = tmp_path / "wordlist.txt" kept.write_text("admin\n", encoding="utf-8") monkeypatch.chdir(tmp_path) _write_run_record( tmp_path / "strix_runs", "pentest_abcd", { "run_name": "pentest_abcd", "targets_info": [], "local_sources": [], "workspace_mount": str(work), "workspace_files": [ {"source_path": str(kept), "workspace_path": "/workspace/lists/words.txt"}, {"source_path": str(tmp_path / "gone.txt"), "workspace_path": "/workspace/g.txt"}, ], }, ) monkeypatch.setattr(sys, "argv", ["strix", "--resume", "pentest_abcd"]) args = cli_main.parse_arguments() assert args.workspace_files == [ {"source_path": str(kept), "workspace_path": "/workspace/lists/words.txt"} ] def test_resume_rejects_an_edited_workspace_file_path( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: """A hand-edited record cannot place a file outside the workspace.""" work = tmp_path / "project" work.mkdir() source = tmp_path / "wordlist.txt" source.write_text("admin\n", encoding="utf-8") monkeypatch.chdir(tmp_path) _write_run_record( tmp_path / "strix_runs", "pentest_abcd", { "run_name": "pentest_abcd", "targets_info": [], "local_sources": [], "workspace_mount": str(work), "workspace_files": [ {"source_path": str(source), "workspace_path": "/etc/cron.d/payload"} ], }, ) monkeypatch.setattr(sys, "argv", ["strix", "--resume", "pentest_abcd"]) with pytest.raises(SystemExit): cli_main.parse_arguments() assert "invalid workspace file" in capsys.readouterr().err def test_resume_reports_a_missing_workspace_directory( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: monkeypatch.chdir(tmp_path) _write_run_record( tmp_path / "strix_runs", "pentest_abcd", { "run_name": "pentest_abcd", "targets_info": [], "local_sources": [], "workspace_mount": str(tmp_path / "deleted"), }, ) monkeypatch.setattr(sys, "argv", ["strix", "--resume", "pentest_abcd"]) with pytest.raises(SystemExit): cli_main.parse_arguments() assert "is missing" in capsys.readouterr().err def test_resume_still_requires_targets_or_a_workspace( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: monkeypatch.chdir(tmp_path) _write_run_record( tmp_path / "strix_runs", "pentest_abcd", {"run_name": "pentest_abcd", "targets_info": [], "local_sources": []}, ) monkeypatch.setattr(sys, "argv", ["strix", "--resume", "pentest_abcd"]) with pytest.raises(SystemExit): cli_main.parse_arguments() assert "has no targets_info" in capsys.readouterr().err