From a0b3db471b8e188f63773ef2d01f4199828e29aa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Sep 2025 08:00:58 +0000 Subject: [PATCH 1/8] Initial plan From 039eaa2514c021e643263443852cfb1ee913ef84 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Sep 2025 08:15:43 +0000 Subject: [PATCH 2/8] Implement complete replay command functionality with comprehensive tests Co-authored-by: ali90h <89179776+ali90h@users.noreply.github.com> --- autorepro/cli.py | 473 +++++++++++++++++++++++++++++++++++++++++++ tests/test_replay.py | 382 ++++++++++++++++++++++++++++++++++ 2 files changed, 855 insertions(+) create mode 100644 tests/test_replay.py diff --git a/autorepro/cli.py b/autorepro/cli.py index 7f2dc9c..bf8b704 100644 --- a/autorepro/cli.py +++ b/autorepro/cli.py @@ -570,6 +570,73 @@ def _setup_report_parser(subparsers) -> argparse.ArgumentParser: return report_parser +def _setup_replay_parser(subparsers) -> argparse.ArgumentParser: + """Setup replay subcommand parser.""" + replay_parser = subparsers.add_parser( + "replay", + help="Re-execute JSONL runs from previous exec sessions", + description="Re-run a previous execution log (runs.jsonl) to reproduce execution results, compare outcomes, and emit a new replay.jsonl plus REPLAY_SUMMARY.json", + ) + + # Required input file + replay_parser.add_argument( + "--from", + dest="from_path", + required=True, + help="Path to input JSONL file containing 'type:run' records", + ) + + # Selection and filtering options + replay_parser.add_argument( + "--until-success", + action="store_true", + help="Stop after the first successful replay (exit_code == 0)", + ) + replay_parser.add_argument( + "--indexes", + help="Filter the selected set before replay (single indices and ranges, comma-separated, e.g., '0,2-3')", + ) + + # Execution options + replay_parser.add_argument( + "--timeout", + type=int, + help="Per-run timeout in seconds (applied to each command)", + ) + + # Output options + replay_parser.add_argument( + "--jsonl", + help="Write per-run replay records and final summary to JSONL file (default: replay.jsonl)", + ) + replay_parser.add_argument( + "--summary", + help="Write a standalone JSON summary file (default: replay_summary.json)", + ) + + # Common options + replay_parser.add_argument( + "--dry-run", + action="store_true", + help="Print actions without executing", + ) + replay_parser.add_argument( + "-q", + "--quiet", + action="store_true", + help="Show errors only", + ) + replay_parser.add_argument( + "-v", + "--verbose", + action="count", + default=0, + help="Increase verbosity (-v, -vv)", + ) + + return replay_parser + + def create_parser() -> argparse.ArgumentParser: """Create and configure the argument parser.""" parser = argparse.ArgumentParser( @@ -604,6 +671,7 @@ def create_parser() -> argparse.ArgumentParser: _setup_plan_parser(subparsers) _setup_exec_parser(subparsers) _setup_report_parser(subparsers) + _setup_replay_parser(subparsers) return parser @@ -947,6 +1015,88 @@ def _parse_indexes(self, indexes_str: str) -> list[int]: # noqa: C901 return sorted(set(result)) +@dataclass +class ReplayConfig: + """Configuration for replay command operations.""" + + from_path: str + until_success: bool = False + indexes: str | None = None + timeout: int = field(default_factory=lambda: config.timeouts.default_seconds) + jsonl_path: str | None = None + summary_path: str | None = None + dry_run: bool = False + + def validate(self) -> None: + """Validate replay configuration and raise descriptive errors.""" + # Required field validation + if not self.from_path: + raise FieldValidationError( + "from_path is required", field="from_path" + ) + + # File existence validation + from pathlib import Path + if not Path(self.from_path).exists(): + raise FieldValidationError( + f"input file does not exist: {self.from_path}", field="from_path" + ) + + # Validate indexes format if provided + if self.indexes: + try: + self._parse_indexes(self.indexes) + except ValueError as e: + raise FieldValidationError( + f"invalid indexes format: {e}", field="indexes" + ) from e + + # Field validation + if self.timeout <= 0: + raise FieldValidationError( + f"timeout must be positive, got: {self.timeout}", field="timeout" + ) + + def _parse_indexes(self, indexes_str: str) -> list[int]: # noqa: C901 + """Parse indexes string like '0,2-3' into list of integers.""" + if not indexes_str.strip(): + raise ValueError("indexes string cannot be empty") + + result: list[int] = [] + for part in indexes_str.split(","): + part = part.strip() + if not part: + continue + + if "-" in part: + # Range like "2-5" + try: + start, end = part.split("-", 1) + start_idx = int(start.strip()) + end_idx = int(end.strip()) + if start_idx < 0 or end_idx < 0: + raise ValueError("indexes must be non-negative") + if start_idx > end_idx: + raise ValueError(f"invalid range {part}: start > end") + result.extend(range(start_idx, end_idx + 1)) + except ValueError as e: + if "invalid range" in str(e): + raise + raise ValueError(f"invalid range format: {part}") from e + else: + # Single index + try: + idx = int(part) + if idx < 0: + raise ValueError("indexes must be non-negative") + result.append(idx) + except ValueError: + raise ValueError(f"invalid index: {part}") from None + + # Remove duplicates and sort + return sorted(set(result)) + + @dataclass @dataclass class InitConfig: @@ -2071,6 +2221,308 @@ def cmd_exec(config: ExecConfig | None = None, **kwargs) -> int: return 1 # Generic error for unexpected failures +def _parse_jsonl_file(file_path: str) -> list[dict]: + """Parse JSONL file and return list of run records.""" + import json + + log = logging.getLogger("autorepro") + run_records = [] + + try: + with open(file_path, encoding="utf-8") as f: + for line_num, line in enumerate(f, 1): + line = line.strip() + if not line: + continue + + try: + record = json.loads(line) + if record.get("type") == "run": + run_records.append(record) + except json.JSONDecodeError as e: + log.warning(f"Skipping invalid JSON on line {line_num}: {e}") + continue + + except OSError as e: + log.error(f"Failed to read JSONL file {file_path}: {e}") + raise + + return run_records + + +def _filter_records_by_indexes(records: list[dict], indexes_str: str | None) -> list[dict]: + """Filter records by index string like '0,2-3'.""" + if not indexes_str: + return records + + config = ReplayConfig(from_path="") # Temporary instance for parsing + try: + selected_indexes = config._parse_indexes(indexes_str) + except ValueError as e: + raise ValueError(f"Invalid indexes format: {e}") from e + + # Filter records that match the selected indexes + filtered = [] + for record in records: + record_index = record.get("index") + if record_index is not None and record_index in selected_indexes: + filtered.append(record) + + return filtered + + +def _execute_replay_command( + cmd: str, timeout: int, repo_path: Path | None = None +) -> dict: + """Execute a command for replay and return results.""" + import subprocess + import time + from datetime import datetime + + log = logging.getLogger("autorepro") + + # Determine execution directory + exec_dir = repo_path if repo_path else Path.cwd() + + # Record start time + start_time = time.time() + start_dt = datetime.now() + + # Execute command with timeout + try: + result = subprocess.run( + cmd, + shell=True, + cwd=exec_dir, + capture_output=True, + text=True, + timeout=timeout, + ) + + end_time = time.time() + end_dt = datetime.now() + duration_ms = int((end_time - start_time) * 1000) + + # Prepare output previews (first 1KB for human inspection) + stdout_preview = result.stdout[:1024] if result.stdout else "" + stderr_preview = result.stderr[:1024] if result.stderr else "" + + return { + "exit_code": result.returncode, + "duration_ms": duration_ms, + "start_time": start_dt, + "end_time": end_dt, + "stdout_full": result.stdout, + "stderr_full": result.stderr, + "stdout_preview": stdout_preview, + "stderr_preview": stderr_preview, + } + + except subprocess.TimeoutExpired: + end_time = time.time() + end_dt = datetime.now() + duration_ms = int((end_time - start_time) * 1000) + + log.warning(f"Command timed out after {timeout}s: {cmd}") + return { + "exit_code": 124, # Standard timeout exit code + "duration_ms": duration_ms, + "start_time": start_dt, + "end_time": end_dt, + "stdout_full": "", + "stderr_full": f"Command timed out after {timeout}s", + "stdout_preview": "", + "stderr_preview": f"Command timed out after {timeout}s", + } + + except Exception as e: + end_time = time.time() + end_dt = datetime.now() + duration_ms = int((end_time - start_time) * 1000) + + log.error(f"Command execution failed: {e}") + return { + "exit_code": 1, + "duration_ms": duration_ms, + "start_time": start_dt, + "end_time": end_dt, + "stdout_full": "", + "stderr_full": f"Execution failed: {e}", + "stdout_preview": "", + "stderr_preview": f"Execution failed: {e}", + } + + +def _create_replay_run_record( + index: int, + cmd: str, + original_exit_code: int, + results: dict, +) -> dict: + """Create a replay run record for JSONL output.""" + return { + "type": "run", + "index": index, + "cmd": cmd, + "start_ts": results["start_time"].strftime("%Y-%m-%dT%H:%M:%SZ"), + "end_ts": results["end_time"].strftime("%Y-%m-%dT%H:%M:%SZ"), + "exit_code_original": original_exit_code, + "exit_code_replay": results["exit_code"], + "matched": original_exit_code == results["exit_code"], + "duration_ms": results["duration_ms"], + "stdout_preview": results["stdout_preview"], + "stderr_preview": results["stderr_preview"], + } + + +def _create_replay_summary_record( # noqa: PLR0913 + runs: int, successes: int, failures: int, matches: int, mismatches: int, first_success_index: int | None +) -> dict: + """Create a replay summary record for JSONL output.""" + return { + "type": "summary", + "schema_version": 1, + "tool": "autorepro", + "mode": "replay", + "runs": runs, + "successes": successes, + "failures": failures, + "matches": matches, + "mismatches": mismatches, + "first_success_index": first_success_index, + } + + +def cmd_replay(config: ReplayConfig) -> int: # noqa: PLR0915, C901, PLR0911, PLR0912 + """Handle the replay command.""" + log = logging.getLogger("autorepro") + + try: + # Validate configuration + config.validate() + + # Parse JSONL file to get run records + try: + run_records = _parse_jsonl_file(config.from_path) + except Exception as e: + log.error(f"Failed to parse JSONL file: {e}") + return 1 + + if not run_records: + log.error("No 'type:run' records found in JSONL file") + return 1 + + # Filter records by indexes if specified + if config.indexes: + try: + run_records = _filter_records_by_indexes(run_records, config.indexes) + except ValueError as e: + log.error(f"Index filtering failed: {e}") + return 1 + + if not run_records: + log.error("No records match the specified indexes") + return 1 + + # Sort records by index to ensure consistent execution order + run_records.sort(key=lambda x: x.get("index", 0)) + + if config.dry_run: + log.info(f"Would replay {len(run_records)} commands:") + for record in run_records: + index = record.get("index", "?") + cmd = record.get("cmd", "") + log.info(f" [{index}] {cmd}") + return 0 + + # Set default output paths if not specified + jsonl_path = config.jsonl_path or "replay.jsonl" + summary_path = config.summary_path or "replay_summary.json" + + # Execute replay + runs = 0 + successes = 0 + failures = 0 + matches = 0 + mismatches = 0 + first_success_index = None + + log.info(f"Starting replay of {len(run_records)} commands") + + for record in run_records: + index = record.get("index", runs) + cmd = record.get("cmd", "") + original_exit_code = record.get("exit_code", 0) + + if not cmd: + log.warning(f"Skipping record {index}: no command found") + continue + + log.info(f"Replaying [{index}]: {cmd}") + + # Execute the command + results = _execute_replay_command(cmd, config.timeout) + replay_exit_code = results["exit_code"] + + # Track statistics + runs += 1 + if replay_exit_code == 0: + successes += 1 + if first_success_index is None: + first_success_index = index + else: + failures += 1 + + if original_exit_code == replay_exit_code: + matches += 1 + else: + mismatches += 1 + + # Create and write replay run record + replay_record = _create_replay_run_record( + index, cmd, original_exit_code, results + ) + + if config.jsonl_path: + _write_jsonl_record(jsonl_path, replay_record) + + # Print output to console unless quiet + if results["stdout_full"]: + print(results["stdout_full"], end="") + if results["stderr_full"]: + print(results["stderr_full"], file=sys.stderr, end="") + + # Check for early stopping + if config.until_success and replay_exit_code == 0: + log.info(f"Stopping after first success (command {index})") + break + + # Create and write summary + summary_record = _create_replay_summary_record( + runs, successes, failures, matches, mismatches, first_success_index + ) + + if config.jsonl_path: + _write_jsonl_record(jsonl_path, summary_record) + + if config.summary_path: + try: + with open(summary_path, "w", encoding="utf-8") as f: + json.dump(summary_record, f, indent=2) + except OSError as e: + log.error(f"Failed to write summary file: {e}") + return 1 + + # Log final results + log.info(f"Replay completed: {runs} runs, {successes} successes, {matches} matches") + + return 0 + + except Exception as e: + log.error(f"Replay failed: {e}") + return 1 + + def _prepare_pr_config(config: PrConfig) -> PrConfig: """Validate and process PR configuration.""" log = logging.getLogger("autorepro") @@ -2506,6 +2958,25 @@ def _setup_logging(args, project_verbosity: str | None = None) -> None: configure_logging(level=level, fmt=None, stream=sys.stderr) +def _dispatch_replay_command(args) -> int: + """Dispatch replay command with parsed arguments.""" + global_config = get_config() + timeout_value = getattr(args, "timeout", None) + if timeout_value is None: + timeout_value = global_config.timeouts.default_seconds + + config = ReplayConfig( + from_path=args.from_path, + until_success=getattr(args, "until_success", False), + indexes=getattr(args, "indexes", None), + timeout=timeout_value, + jsonl_path=getattr(args, "jsonl", None), + summary_path=getattr(args, "summary", None), + dry_run=getattr(args, "dry_run", False), + ) + return cmd_replay(config) + + def _dispatch_command(args, parser) -> int: # noqa: PLR0911 """Dispatch command based on parsed arguments.""" if args.command == "scan": @@ -2520,6 +2991,8 @@ def _dispatch_command(args, parser) -> int: # noqa: PLR0911 return _dispatch_pr_command(args) elif args.command == "report": return _dispatch_report_command(args) + elif args.command == "replay": + return _dispatch_replay_command(args) return _dispatch_help_command(parser) diff --git a/tests/test_replay.py b/tests/test_replay.py new file mode 100644 index 0000000..45dea4d --- /dev/null +++ b/tests/test_replay.py @@ -0,0 +1,382 @@ +"""Tests for replay command functionality.""" + +import json +import os +import tempfile +from pathlib import Path + +import pytest + +from autorepro.cli import ReplayConfig, cmd_replay, _parse_jsonl_file, _filter_records_by_indexes, _create_replay_run_record, _create_replay_summary_record +from autorepro.config.exceptions import FieldValidationError + + +class TestReplayConfig: + """Test ReplayConfig validation and parsing.""" + + def test_valid_config(self): + """Test valid replay configuration.""" + with tempfile.TemporaryDirectory() as tmpdir: + test_file = Path(tmpdir) / "test.jsonl" + test_file.write_text('{"type": "run", "index": 0, "cmd": "echo test", "exit_code": 0}\n') + + config = ReplayConfig(from_path=str(test_file)) + config.validate() # Should not raise + + def test_missing_from_path(self): + """Test validation fails when from_path is missing.""" + config = ReplayConfig(from_path="") + with pytest.raises(FieldValidationError, match="from_path is required"): + config.validate() + + def test_nonexistent_file(self): + """Test validation fails when file doesn't exist.""" + config = ReplayConfig(from_path="/nonexistent/file.jsonl") + with pytest.raises(FieldValidationError, match="input file does not exist"): + config.validate() + + def test_invalid_timeout(self): + """Test validation fails with invalid timeout.""" + with tempfile.TemporaryDirectory() as tmpdir: + test_file = Path(tmpdir) / "test.jsonl" + test_file.write_text('{"type": "run", "index": 0, "cmd": "echo test", "exit_code": 0}\n') + + config = ReplayConfig(from_path=str(test_file), timeout=0) + with pytest.raises(FieldValidationError, match="timeout must be positive"): + config.validate() + + def test_valid_indexes_parsing(self): + """Test valid indexes string parsing.""" + with tempfile.TemporaryDirectory() as tmpdir: + test_file = Path(tmpdir) / "test.jsonl" + test_file.write_text('{"type": "run", "index": 0, "cmd": "echo test", "exit_code": 0}\n') + + config = ReplayConfig(from_path=str(test_file), indexes="0,2-4,7") + result = config._parse_indexes("0,2-4,7") + expected = [0, 2, 3, 4, 7] + assert result == expected + + def test_invalid_indexes_parsing(self): + """Test invalid indexes string parsing.""" + with tempfile.TemporaryDirectory() as tmpdir: + test_file = Path(tmpdir) / "test.jsonl" + test_file.write_text('{"type": "run", "index": 0, "cmd": "echo test", "exit_code": 0}\n') + + config = ReplayConfig(from_path=str(test_file), indexes="invalid") + with pytest.raises(FieldValidationError, match="invalid indexes format"): + config.validate() + + +class TestJSONLParsing: + """Test JSONL file parsing functionality.""" + + def test_parse_valid_jsonl(self): + """Test parsing valid JSONL file with run records.""" + with tempfile.TemporaryDirectory() as tmpdir: + test_file = Path(tmpdir) / "test.jsonl" + content = '''{"type": "run", "index": 0, "cmd": "echo test1", "exit_code": 0} +{"type": "run", "index": 1, "cmd": "echo test2", "exit_code": 1} +{"type": "summary", "runs": 2} +''' + test_file.write_text(content) + + records = _parse_jsonl_file(str(test_file)) + assert len(records) == 2 + assert records[0]["cmd"] == "echo test1" + assert records[1]["cmd"] == "echo test2" + + def test_parse_empty_file(self): + """Test parsing empty JSONL file.""" + with tempfile.TemporaryDirectory() as tmpdir: + test_file = Path(tmpdir) / "empty.jsonl" + test_file.write_text("") + + records = _parse_jsonl_file(str(test_file)) + assert records == [] + + def test_parse_no_run_records(self): + """Test parsing JSONL file with no run records.""" + with tempfile.TemporaryDirectory() as tmpdir: + test_file = Path(tmpdir) / "test.jsonl" + content = '''{"type": "summary", "runs": 0} +{"type": "other", "data": "value"} +''' + test_file.write_text(content) + + records = _parse_jsonl_file(str(test_file)) + assert records == [] + + def test_parse_invalid_json_lines(self): + """Test parsing JSONL file with some invalid JSON lines.""" + with tempfile.TemporaryDirectory() as tmpdir: + test_file = Path(tmpdir) / "test.jsonl" + content = '''{"type": "run", "index": 0, "cmd": "echo test1", "exit_code": 0} +invalid json line +{"type": "run", "index": 1, "cmd": "echo test2", "exit_code": 1} +''' + test_file.write_text(content) + + records = _parse_jsonl_file(str(test_file)) + assert len(records) == 2 + assert records[0]["cmd"] == "echo test1" + assert records[1]["cmd"] == "echo test2" + + +class TestIndexFiltering: + """Test index filtering functionality.""" + + def test_filter_by_single_index(self): + """Test filtering by single index.""" + records = [ + {"type": "run", "index": 0, "cmd": "cmd0"}, + {"type": "run", "index": 1, "cmd": "cmd1"}, + {"type": "run", "index": 2, "cmd": "cmd2"}, + ] + + filtered = _filter_records_by_indexes(records, "1") + assert len(filtered) == 1 + assert filtered[0]["cmd"] == "cmd1" + + def test_filter_by_range(self): + """Test filtering by range.""" + records = [ + {"type": "run", "index": 0, "cmd": "cmd0"}, + {"type": "run", "index": 1, "cmd": "cmd1"}, + {"type": "run", "index": 2, "cmd": "cmd2"}, + {"type": "run", "index": 3, "cmd": "cmd3"}, + ] + + filtered = _filter_records_by_indexes(records, "1-2") + assert len(filtered) == 2 + assert filtered[0]["cmd"] == "cmd1" + assert filtered[1]["cmd"] == "cmd2" + + def test_filter_by_mixed_indexes(self): + """Test filtering by mixed indexes and ranges.""" + records = [ + {"type": "run", "index": 0, "cmd": "cmd0"}, + {"type": "run", "index": 1, "cmd": "cmd1"}, + {"type": "run", "index": 2, "cmd": "cmd2"}, + {"type": "run", "index": 3, "cmd": "cmd3"}, + {"type": "run", "index": 4, "cmd": "cmd4"}, + ] + + filtered = _filter_records_by_indexes(records, "0,2-3") + assert len(filtered) == 3 + assert filtered[0]["cmd"] == "cmd0" + assert filtered[1]["cmd"] == "cmd2" + assert filtered[2]["cmd"] == "cmd3" + + def test_filter_no_matches(self): + """Test filtering with no matching indexes.""" + records = [ + {"type": "run", "index": 0, "cmd": "cmd0"}, + {"type": "run", "index": 1, "cmd": "cmd1"}, + ] + + filtered = _filter_records_by_indexes(records, "5-7") + assert filtered == [] + + def test_filter_no_indexes_specified(self): + """Test filtering with no indexes specified returns all records.""" + records = [ + {"type": "run", "index": 0, "cmd": "cmd0"}, + {"type": "run", "index": 1, "cmd": "cmd1"}, + ] + + filtered = _filter_records_by_indexes(records, None) + assert filtered == records + + +class TestRecordCreation: + """Test replay record creation functions.""" + + def test_create_replay_run_record(self): + """Test creation of replay run records.""" + from datetime import datetime + + results = { + "exit_code": 1, + "duration_ms": 150, + "start_time": datetime(2025, 9, 15, 12, 0, 0), + "end_time": datetime(2025, 9, 15, 12, 0, 1), + "stdout_preview": "output", + "stderr_preview": "error", + } + + record = _create_replay_run_record(2, "test command", 0, results) + + assert record["type"] == "run" + assert record["index"] == 2 + assert record["cmd"] == "test command" + assert record["exit_code_original"] == 0 + assert record["exit_code_replay"] == 1 + assert record["matched"] is False + assert record["duration_ms"] == 150 + assert record["stdout_preview"] == "output" + assert record["stderr_preview"] == "error" + + def test_create_replay_summary_record(self): + """Test creation of replay summary records.""" + record = _create_replay_summary_record(5, 3, 2, 4, 1, 1) + + assert record["type"] == "summary" + assert record["schema_version"] == 1 + assert record["tool"] == "autorepro" + assert record["mode"] == "replay" + assert record["runs"] == 5 + assert record["successes"] == 3 + assert record["failures"] == 2 + assert record["matches"] == 4 + assert record["mismatches"] == 1 + assert record["first_success_index"] == 1 + + +class TestReplayExecution: + """Test end-to-end replay execution.""" + + def test_replay_dry_run(self): + """Test replay dry run functionality.""" + with tempfile.TemporaryDirectory() as tmpdir: + # Create test JSONL file + test_file = Path(tmpdir) / "test.jsonl" + content = '''{"type": "run", "index": 0, "cmd": "echo test1", "exit_code": 0} +{"type": "run", "index": 1, "cmd": "echo test2", "exit_code": 1} +''' + test_file.write_text(content) + + config = ReplayConfig(from_path=str(test_file), dry_run=True) + result = cmd_replay(config) + + assert result == 0 + + def test_replay_simple_execution(self): + """Test simple replay execution.""" + with tempfile.TemporaryDirectory() as tmpdir: + # Create test JSONL file + test_file = Path(tmpdir) / "test.jsonl" + content = '''{"type": "run", "index": 0, "cmd": "echo 'success'", "exit_code": 0} +{"type": "run", "index": 1, "cmd": "python -c 'import sys; sys.exit(1)'", "exit_code": 1} +''' + test_file.write_text(content) + + # Create output files + jsonl_file = Path(tmpdir) / "replay.jsonl" + summary_file = Path(tmpdir) / "summary.json" + + config = ReplayConfig( + from_path=str(test_file), + jsonl_path=str(jsonl_file), + summary_path=str(summary_file), + ) + result = cmd_replay(config) + + assert result == 0 + assert jsonl_file.exists() + assert summary_file.exists() + + # Verify JSONL output + jsonl_lines = jsonl_file.read_text().strip().split('\n') + assert len(jsonl_lines) == 3 # 2 run records + 1 summary + + # Verify summary + summary = json.loads(summary_file.read_text()) + assert summary["runs"] == 2 + assert summary["successes"] == 1 + assert summary["failures"] == 1 + + def test_replay_with_index_filtering(self): + """Test replay with index filtering.""" + with tempfile.TemporaryDirectory() as tmpdir: + # Create test JSONL file + test_file = Path(tmpdir) / "test.jsonl" + content = '''{"type": "run", "index": 0, "cmd": "echo 'cmd0'", "exit_code": 0} +{"type": "run", "index": 1, "cmd": "echo 'cmd1'", "exit_code": 0} +{"type": "run", "index": 2, "cmd": "echo 'cmd2'", "exit_code": 0} +''' + test_file.write_text(content) + + summary_file = Path(tmpdir) / "summary.json" + + config = ReplayConfig( + from_path=str(test_file), + indexes="0,2", + summary_path=str(summary_file), + ) + result = cmd_replay(config) + + assert result == 0 + + # Verify only 2 commands were run + summary = json.loads(summary_file.read_text()) + assert summary["runs"] == 2 + + def test_replay_until_success(self): + """Test replay with until-success flag.""" + with tempfile.TemporaryDirectory() as tmpdir: + # Create test JSONL file - first command fails, second succeeds + test_file = Path(tmpdir) / "test.jsonl" + content = '''{"type": "run", "index": 0, "cmd": "python -c 'import sys; sys.exit(1)'", "exit_code": 1} +{"type": "run", "index": 1, "cmd": "echo 'success'", "exit_code": 0} +{"type": "run", "index": 2, "cmd": "echo 'should not run'", "exit_code": 0} +''' + test_file.write_text(content) + + summary_file = Path(tmpdir) / "summary.json" + + config = ReplayConfig( + from_path=str(test_file), + until_success=True, + summary_path=str(summary_file), + ) + result = cmd_replay(config) + + assert result == 0 + + # Should stop after second command (first success) + summary = json.loads(summary_file.read_text()) + assert summary["runs"] == 2 + assert summary["successes"] == 1 + assert summary["first_success_index"] == 1 + + def test_replay_error_cases(self): + """Test replay error handling.""" + # Test with nonexistent file + config = ReplayConfig(from_path="/nonexistent/file.jsonl") + result = cmd_replay(config) + assert result == 1 + + # Test with empty JSONL file + with tempfile.TemporaryDirectory() as tmpdir: + test_file = Path(tmpdir) / "empty.jsonl" + test_file.write_text("") + + config = ReplayConfig(from_path=str(test_file)) + result = cmd_replay(config) + assert result == 1 + + def test_replay_invalid_indexes(self): + """Test replay with invalid indexes.""" + with tempfile.TemporaryDirectory() as tmpdir: + # Create test JSONL file + test_file = Path(tmpdir) / "test.jsonl" + content = '''{"type": "run", "index": 0, "cmd": "echo test", "exit_code": 0} +''' + test_file.write_text(content) + + config = ReplayConfig(from_path=str(test_file), indexes="invalid") + result = cmd_replay(config) + assert result == 1 + + def test_replay_no_matching_indexes(self): + """Test replay when no records match specified indexes.""" + with tempfile.TemporaryDirectory() as tmpdir: + # Create test JSONL file + test_file = Path(tmpdir) / "test.jsonl" + content = '''{"type": "run", "index": 0, "cmd": "echo test", "exit_code": 0} +''' + test_file.write_text(content) + + config = ReplayConfig(from_path=str(test_file), indexes="5-7") + result = cmd_replay(config) + assert result == 1 \ No newline at end of file From ea48b6f4df4bc85f69bf7dac90df876923aef34c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Sep 2025 08:22:43 +0000 Subject: [PATCH 3/8] Fix linting and formatting issues in replay command implementation - Fixed import ordering and removed unused imports in tests/test_replay.py - Cleaned up whitespace and applied consistent string formatting - Applied black code formatting to all affected files - Added missing newline at end of test file - All tests still pass (719 tests) Co-authored-by: ali90h <89179776+ali90h@users.noreply.github.com> --- autorepro/cli.py | 20 +++-- autorepro/detect.py | 38 ++++---- autorepro/utils/error_handling.py | 8 +- autorepro/utils/logging.py | 8 +- tests/test_replay.py | 138 ++++++++++++++++-------------- 5 files changed, 124 insertions(+), 88 deletions(-) diff --git a/autorepro/cli.py b/autorepro/cli.py index bf8b704..e6831fa 100644 --- a/autorepro/cli.py +++ b/autorepro/cli.py @@ -1031,12 +1031,11 @@ def validate(self) -> None: """Validate replay configuration and raise descriptive errors.""" # Required field validation if not self.from_path: - raise FieldValidationError( - "from_path is required", field="from_path" - ) + raise FieldValidationError("from_path is required", field="from_path") # File existence validation from pathlib import Path + if not Path(self.from_path).exists(): raise FieldValidationError( f"input file does not exist: {self.from_path}", field="from_path" @@ -2250,7 +2249,9 @@ def _parse_jsonl_file(file_path: str) -> list[dict]: return run_records -def _filter_records_by_indexes(records: list[dict], indexes_str: str | None) -> list[dict]: +def _filter_records_by_indexes( + records: list[dict], indexes_str: str | None +) -> list[dict]: """Filter records by index string like '0,2-3'.""" if not indexes_str: return records @@ -2376,7 +2377,12 @@ def _create_replay_run_record( def _create_replay_summary_record( # noqa: PLR0913 - runs: int, successes: int, failures: int, matches: int, mismatches: int, first_success_index: int | None + runs: int, + successes: int, + failures: int, + matches: int, + mismatches: int, + first_success_index: int | None, ) -> dict: """Create a replay summary record for JSONL output.""" return { @@ -2514,7 +2520,9 @@ def cmd_replay(config: ReplayConfig) -> int: # noqa: PLR0915, C901, PLR0911, PL return 1 # Log final results - log.info(f"Replay completed: {runs} runs, {successes} successes, {matches} matches") + log.info( + f"Replay completed: {runs} runs, {successes} successes, {matches} matches" + ) return 0 diff --git a/autorepro/detect.py b/autorepro/detect.py index 9d1e86d..df8e36f 100644 --- a/autorepro/detect.py +++ b/autorepro/detect.py @@ -225,9 +225,11 @@ def _process_weighted_patterns( pattern=filename, path=f"./{filename}", kind=str(info["kind"]), - weight=int(info["weight"]) - if isinstance(info["weight"], int | str) - else 0, + weight=( + int(info["weight"]) + if isinstance(info["weight"], int | str) + else 0 + ), ), ) @@ -265,9 +267,11 @@ def _process_glob_pattern( pattern=pattern, path=f"./{basename}", kind=str(info["kind"]), - weight=int(info["weight"]) - if isinstance(info["weight"], int | str) - else 0, + weight=( + int(info["weight"]) + if isinstance(info["weight"], int | str) + else 0 + ), ), ) @@ -289,9 +293,9 @@ def _process_exact_filename( pattern=pattern, path=f"./{pattern}", kind=str(info["kind"]), - weight=int(info["weight"]) - if isinstance(info["weight"], int | str) - else 0, + weight=( + int(info["weight"]) if isinstance(info["weight"], int | str) else 0 + ), ), ) @@ -569,9 +573,11 @@ def collect_evidence( # noqa: C901 pattern=filename, path=rel_path, kind=str(info["kind"]), - weight=int(info["weight"]) - if isinstance(info["weight"], int | str) - else 0, + weight=( + int(info["weight"]) + if isinstance(info["weight"], int | str) + else 0 + ), ), ) @@ -595,9 +601,11 @@ def collect_evidence( # noqa: C901 pattern=pattern, path=rel_path, kind=str(info["kind"]), - weight=int(info["weight"]) - if isinstance(info["weight"], int | str) - else 0, + weight=( + int(info["weight"]) + if isinstance(info["weight"], int | str) + else 0 + ), ), ) else: diff --git a/autorepro/utils/error_handling.py b/autorepro/utils/error_handling.py index c317ae9..d20585a 100644 --- a/autorepro/utils/error_handling.py +++ b/autorepro/utils/error_handling.py @@ -295,9 +295,11 @@ def _safe_subprocess_run_impl( errno_val == 1 or "not permitted" in str(e).lower() ): te = subprocess.TimeoutExpired( - cmd - if isinstance(cmd, list) - else (cmd.split() if isinstance(cmd, str) else cmd), + ( + cmd + if isinstance(cmd, list) + else (cmd.split() if isinstance(cmd, str) else cmd) + ), config.timeout, ) error_msg = f"{operation_name} timed out after {config.timeout}s: {cmd_str}" diff --git a/autorepro/utils/logging.py b/autorepro/utils/logging.py index 307f923..6ed07cc 100644 --- a/autorepro/utils/logging.py +++ b/autorepro/utils/logging.py @@ -63,7 +63,9 @@ def format(self, record: logging.LogRecord) -> str: return json.dumps(payload, separators=(",", ":")) - def formatTime(self, record: logging.LogRecord, datefmt: str | None = None) -> str: # noqa: N802 + def formatTime( + self, record: logging.LogRecord, datefmt: str | None = None + ) -> str: # noqa: N802 ct = self.converter(record.created) if datefmt: s = time.strftime(datefmt, ct) @@ -103,7 +105,9 @@ def format(self, record: logging.LogRecord) -> str: pass return base + (" " + " ".join(extras) if extras else "") - def formatTime(self, record: logging.LogRecord, datefmt: str | None = None) -> str: # noqa: N802 + def formatTime( + self, record: logging.LogRecord, datefmt: str | None = None + ) -> str: # noqa: N802 # ISO8601-ish UTC time ct = time.gmtime(record.created) return time.strftime("%Y-%m-%dT%H:%M:%S", ct) + f".{int(record.msecs):03d}Z" diff --git a/tests/test_replay.py b/tests/test_replay.py index 45dea4d..c49645b 100644 --- a/tests/test_replay.py +++ b/tests/test_replay.py @@ -1,13 +1,19 @@ """Tests for replay command functionality.""" import json -import os import tempfile from pathlib import Path import pytest -from autorepro.cli import ReplayConfig, cmd_replay, _parse_jsonl_file, _filter_records_by_indexes, _create_replay_run_record, _create_replay_summary_record +from autorepro.cli import ( + ReplayConfig, + _create_replay_run_record, + _create_replay_summary_record, + _filter_records_by_indexes, + _parse_jsonl_file, + cmd_replay, +) from autorepro.config.exceptions import FieldValidationError @@ -18,8 +24,10 @@ def test_valid_config(self): """Test valid replay configuration.""" with tempfile.TemporaryDirectory() as tmpdir: test_file = Path(tmpdir) / "test.jsonl" - test_file.write_text('{"type": "run", "index": 0, "cmd": "echo test", "exit_code": 0}\n') - + test_file.write_text( + '{"type": "run", "index": 0, "cmd": "echo test", "exit_code": 0}\n' + ) + config = ReplayConfig(from_path=str(test_file)) config.validate() # Should not raise @@ -39,8 +47,10 @@ def test_invalid_timeout(self): """Test validation fails with invalid timeout.""" with tempfile.TemporaryDirectory() as tmpdir: test_file = Path(tmpdir) / "test.jsonl" - test_file.write_text('{"type": "run", "index": 0, "cmd": "echo test", "exit_code": 0}\n') - + test_file.write_text( + '{"type": "run", "index": 0, "cmd": "echo test", "exit_code": 0}\n' + ) + config = ReplayConfig(from_path=str(test_file), timeout=0) with pytest.raises(FieldValidationError, match="timeout must be positive"): config.validate() @@ -49,8 +59,10 @@ def test_valid_indexes_parsing(self): """Test valid indexes string parsing.""" with tempfile.TemporaryDirectory() as tmpdir: test_file = Path(tmpdir) / "test.jsonl" - test_file.write_text('{"type": "run", "index": 0, "cmd": "echo test", "exit_code": 0}\n') - + test_file.write_text( + '{"type": "run", "index": 0, "cmd": "echo test", "exit_code": 0}\n' + ) + config = ReplayConfig(from_path=str(test_file), indexes="0,2-4,7") result = config._parse_indexes("0,2-4,7") expected = [0, 2, 3, 4, 7] @@ -60,8 +72,10 @@ def test_invalid_indexes_parsing(self): """Test invalid indexes string parsing.""" with tempfile.TemporaryDirectory() as tmpdir: test_file = Path(tmpdir) / "test.jsonl" - test_file.write_text('{"type": "run", "index": 0, "cmd": "echo test", "exit_code": 0}\n') - + test_file.write_text( + '{"type": "run", "index": 0, "cmd": "echo test", "exit_code": 0}\n' + ) + config = ReplayConfig(from_path=str(test_file), indexes="invalid") with pytest.raises(FieldValidationError, match="invalid indexes format"): config.validate() @@ -74,12 +88,12 @@ def test_parse_valid_jsonl(self): """Test parsing valid JSONL file with run records.""" with tempfile.TemporaryDirectory() as tmpdir: test_file = Path(tmpdir) / "test.jsonl" - content = '''{"type": "run", "index": 0, "cmd": "echo test1", "exit_code": 0} + content = """{"type": "run", "index": 0, "cmd": "echo test1", "exit_code": 0} {"type": "run", "index": 1, "cmd": "echo test2", "exit_code": 1} {"type": "summary", "runs": 2} -''' +""" test_file.write_text(content) - + records = _parse_jsonl_file(str(test_file)) assert len(records) == 2 assert records[0]["cmd"] == "echo test1" @@ -90,7 +104,7 @@ def test_parse_empty_file(self): with tempfile.TemporaryDirectory() as tmpdir: test_file = Path(tmpdir) / "empty.jsonl" test_file.write_text("") - + records = _parse_jsonl_file(str(test_file)) assert records == [] @@ -98,11 +112,11 @@ def test_parse_no_run_records(self): """Test parsing JSONL file with no run records.""" with tempfile.TemporaryDirectory() as tmpdir: test_file = Path(tmpdir) / "test.jsonl" - content = '''{"type": "summary", "runs": 0} + content = """{"type": "summary", "runs": 0} {"type": "other", "data": "value"} -''' +""" test_file.write_text(content) - + records = _parse_jsonl_file(str(test_file)) assert records == [] @@ -110,12 +124,12 @@ def test_parse_invalid_json_lines(self): """Test parsing JSONL file with some invalid JSON lines.""" with tempfile.TemporaryDirectory() as tmpdir: test_file = Path(tmpdir) / "test.jsonl" - content = '''{"type": "run", "index": 0, "cmd": "echo test1", "exit_code": 0} + content = """{"type": "run", "index": 0, "cmd": "echo test1", "exit_code": 0} invalid json line {"type": "run", "index": 1, "cmd": "echo test2", "exit_code": 1} -''' +""" test_file.write_text(content) - + records = _parse_jsonl_file(str(test_file)) assert len(records) == 2 assert records[0]["cmd"] == "echo test1" @@ -132,7 +146,7 @@ def test_filter_by_single_index(self): {"type": "run", "index": 1, "cmd": "cmd1"}, {"type": "run", "index": 2, "cmd": "cmd2"}, ] - + filtered = _filter_records_by_indexes(records, "1") assert len(filtered) == 1 assert filtered[0]["cmd"] == "cmd1" @@ -145,7 +159,7 @@ def test_filter_by_range(self): {"type": "run", "index": 2, "cmd": "cmd2"}, {"type": "run", "index": 3, "cmd": "cmd3"}, ] - + filtered = _filter_records_by_indexes(records, "1-2") assert len(filtered) == 2 assert filtered[0]["cmd"] == "cmd1" @@ -160,7 +174,7 @@ def test_filter_by_mixed_indexes(self): {"type": "run", "index": 3, "cmd": "cmd3"}, {"type": "run", "index": 4, "cmd": "cmd4"}, ] - + filtered = _filter_records_by_indexes(records, "0,2-3") assert len(filtered) == 3 assert filtered[0]["cmd"] == "cmd0" @@ -173,7 +187,7 @@ def test_filter_no_matches(self): {"type": "run", "index": 0, "cmd": "cmd0"}, {"type": "run", "index": 1, "cmd": "cmd1"}, ] - + filtered = _filter_records_by_indexes(records, "5-7") assert filtered == [] @@ -183,7 +197,7 @@ def test_filter_no_indexes_specified(self): {"type": "run", "index": 0, "cmd": "cmd0"}, {"type": "run", "index": 1, "cmd": "cmd1"}, ] - + filtered = _filter_records_by_indexes(records, None) assert filtered == records @@ -194,7 +208,7 @@ class TestRecordCreation: def test_create_replay_run_record(self): """Test creation of replay run records.""" from datetime import datetime - + results = { "exit_code": 1, "duration_ms": 150, @@ -203,9 +217,9 @@ def test_create_replay_run_record(self): "stdout_preview": "output", "stderr_preview": "error", } - + record = _create_replay_run_record(2, "test command", 0, results) - + assert record["type"] == "run" assert record["index"] == 2 assert record["cmd"] == "test command" @@ -219,7 +233,7 @@ def test_create_replay_run_record(self): def test_create_replay_summary_record(self): """Test creation of replay summary records.""" record = _create_replay_summary_record(5, 3, 2, 4, 1, 1) - + assert record["type"] == "summary" assert record["schema_version"] == 1 assert record["tool"] == "autorepro" @@ -240,14 +254,14 @@ def test_replay_dry_run(self): with tempfile.TemporaryDirectory() as tmpdir: # Create test JSONL file test_file = Path(tmpdir) / "test.jsonl" - content = '''{"type": "run", "index": 0, "cmd": "echo test1", "exit_code": 0} + content = """{"type": "run", "index": 0, "cmd": "echo test1", "exit_code": 0} {"type": "run", "index": 1, "cmd": "echo test2", "exit_code": 1} -''' +""" test_file.write_text(content) - + config = ReplayConfig(from_path=str(test_file), dry_run=True) result = cmd_replay(config) - + assert result == 0 def test_replay_simple_execution(self): @@ -255,30 +269,30 @@ def test_replay_simple_execution(self): with tempfile.TemporaryDirectory() as tmpdir: # Create test JSONL file test_file = Path(tmpdir) / "test.jsonl" - content = '''{"type": "run", "index": 0, "cmd": "echo 'success'", "exit_code": 0} + content = """{"type": "run", "index": 0, "cmd": "echo 'success'", "exit_code": 0} {"type": "run", "index": 1, "cmd": "python -c 'import sys; sys.exit(1)'", "exit_code": 1} -''' +""" test_file.write_text(content) - + # Create output files jsonl_file = Path(tmpdir) / "replay.jsonl" summary_file = Path(tmpdir) / "summary.json" - + config = ReplayConfig( from_path=str(test_file), jsonl_path=str(jsonl_file), summary_path=str(summary_file), ) result = cmd_replay(config) - + assert result == 0 assert jsonl_file.exists() assert summary_file.exists() - + # Verify JSONL output - jsonl_lines = jsonl_file.read_text().strip().split('\n') + jsonl_lines = jsonl_file.read_text().strip().split("\n") assert len(jsonl_lines) == 3 # 2 run records + 1 summary - + # Verify summary summary = json.loads(summary_file.read_text()) assert summary["runs"] == 2 @@ -290,23 +304,23 @@ def test_replay_with_index_filtering(self): with tempfile.TemporaryDirectory() as tmpdir: # Create test JSONL file test_file = Path(tmpdir) / "test.jsonl" - content = '''{"type": "run", "index": 0, "cmd": "echo 'cmd0'", "exit_code": 0} + content = """{"type": "run", "index": 0, "cmd": "echo 'cmd0'", "exit_code": 0} {"type": "run", "index": 1, "cmd": "echo 'cmd1'", "exit_code": 0} {"type": "run", "index": 2, "cmd": "echo 'cmd2'", "exit_code": 0} -''' +""" test_file.write_text(content) - + summary_file = Path(tmpdir) / "summary.json" - + config = ReplayConfig( from_path=str(test_file), indexes="0,2", summary_path=str(summary_file), ) result = cmd_replay(config) - + assert result == 0 - + # Verify only 2 commands were run summary = json.loads(summary_file.read_text()) assert summary["runs"] == 2 @@ -316,23 +330,23 @@ def test_replay_until_success(self): with tempfile.TemporaryDirectory() as tmpdir: # Create test JSONL file - first command fails, second succeeds test_file = Path(tmpdir) / "test.jsonl" - content = '''{"type": "run", "index": 0, "cmd": "python -c 'import sys; sys.exit(1)'", "exit_code": 1} + content = """{"type": "run", "index": 0, "cmd": "python -c 'import sys; sys.exit(1)'", "exit_code": 1} {"type": "run", "index": 1, "cmd": "echo 'success'", "exit_code": 0} {"type": "run", "index": 2, "cmd": "echo 'should not run'", "exit_code": 0} -''' +""" test_file.write_text(content) - + summary_file = Path(tmpdir) / "summary.json" - + config = ReplayConfig( from_path=str(test_file), until_success=True, summary_path=str(summary_file), ) result = cmd_replay(config) - + assert result == 0 - + # Should stop after second command (first success) summary = json.loads(summary_file.read_text()) assert summary["runs"] == 2 @@ -345,12 +359,12 @@ def test_replay_error_cases(self): config = ReplayConfig(from_path="/nonexistent/file.jsonl") result = cmd_replay(config) assert result == 1 - + # Test with empty JSONL file with tempfile.TemporaryDirectory() as tmpdir: test_file = Path(tmpdir) / "empty.jsonl" test_file.write_text("") - + config = ReplayConfig(from_path=str(test_file)) result = cmd_replay(config) assert result == 1 @@ -360,10 +374,10 @@ def test_replay_invalid_indexes(self): with tempfile.TemporaryDirectory() as tmpdir: # Create test JSONL file test_file = Path(tmpdir) / "test.jsonl" - content = '''{"type": "run", "index": 0, "cmd": "echo test", "exit_code": 0} -''' + content = """{"type": "run", "index": 0, "cmd": "echo test", "exit_code": 0} +""" test_file.write_text(content) - + config = ReplayConfig(from_path=str(test_file), indexes="invalid") result = cmd_replay(config) assert result == 1 @@ -373,10 +387,10 @@ def test_replay_no_matching_indexes(self): with tempfile.TemporaryDirectory() as tmpdir: # Create test JSONL file test_file = Path(tmpdir) / "test.jsonl" - content = '''{"type": "run", "index": 0, "cmd": "echo test", "exit_code": 0} -''' + content = """{"type": "run", "index": 0, "cmd": "echo test", "exit_code": 0} +""" test_file.write_text(content) - + config = ReplayConfig(from_path=str(test_file), indexes="5-7") result = cmd_replay(config) - assert result == 1 \ No newline at end of file + assert result == 1 From 9a5dc4bb786c19203e477e1cb5c56aa71e3b3cdd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Sep 2025 08:31:23 +0000 Subject: [PATCH 4/8] Fix robust logging formatTime method to handle missing msecs attribute - Added safe access to record.msecs using getattr with fallback calculation - Prevents CI failures when LogRecord objects don't have msecs attribute - Maintains backward compatibility and proper millisecond formatting - All 719 tests pass, linting and formatting checks pass Co-authored-by: ali90h <89179776+ali90h@users.noreply.github.com> --- autorepro/utils/logging.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/autorepro/utils/logging.py b/autorepro/utils/logging.py index 6ed07cc..9e8a6da 100644 --- a/autorepro/utils/logging.py +++ b/autorepro/utils/logging.py @@ -110,7 +110,10 @@ def formatTime( ) -> str: # noqa: N802 # ISO8601-ish UTC time ct = time.gmtime(record.created) - return time.strftime("%Y-%m-%dT%H:%M:%S", ct) + f".{int(record.msecs):03d}Z" + msecs = getattr( + record, "msecs", int((record.created - int(record.created)) * 1000) + ) + return time.strftime("%Y-%m-%dT%H:%M:%S", ct) + f".{int(msecs):03d}Z" class ContextAdapter(logging.LoggerAdapter): From 9897ddae2a79771b026ecccd8b93fea8bf85567c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Sep 2025 08:43:31 +0000 Subject: [PATCH 5/8] Fix CI failure by improving formatTime method robustness in logging formatters Co-authored-by: ali90h <89179776+ali90h@users.noreply.github.com> --- autorepro/utils/logging.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/autorepro/utils/logging.py b/autorepro/utils/logging.py index 9e8a6da..4edd1ad 100644 --- a/autorepro/utils/logging.py +++ b/autorepro/utils/logging.py @@ -71,7 +71,11 @@ def formatTime( s = time.strftime(datefmt, ct) else: s = time.strftime(self.default_time_format, ct) - return self.default_msec_format % (s, record.msecs) + # Safe access to msecs with fallback calculation for compatibility + msecs = getattr( + record, "msecs", int((record.created - int(record.created)) * 1000) + ) + return self.default_msec_format % (s, msecs) def converter(self, timestamp: float | None): # Use UTC timestamps for easier aggregation in logs @@ -108,8 +112,9 @@ def format(self, record: logging.LogRecord) -> str: def formatTime( self, record: logging.LogRecord, datefmt: str | None = None ) -> str: # noqa: N802 - # ISO8601-ish UTC time + # ISO8601-ish UTC time with robust msecs handling ct = time.gmtime(record.created) + # Safe access to msecs with fallback calculation for compatibility msecs = getattr( record, "msecs", int((record.created - int(record.created)) * 1000) ) From 1235b053c0630ec93e8fe65c2be28f95a1dc531b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Sep 2025 09:11:27 +0000 Subject: [PATCH 6/8] Fix CI compatibility by updating type annotations for better pre-commit support Co-authored-by: ali90h <89179776+ali90h@users.noreply.github.com> --- autorepro/utils/logging.py | 10 +- test_env_and_node.py | 30 +++--- tests/test_cli_smoke.py | 12 +-- tests/test_cli_verbosity.py | 24 ++--- tests/test_config_behavioral.py | 12 +-- tests/test_exit_codes_integration.py | 96 +++++++++--------- tests/test_file_path_resolution.py | 42 ++++---- tests/test_focused_implementation.py | 72 +++++++------- tests/test_golden_plan.py | 12 +-- tests/test_init.py | 12 +-- tests/test_plan_cli.py | 126 ++++++++++++------------ tests/test_plan_core.py | 30 +++--- tests/test_plan_json_cli.py | 12 +-- tests/test_plan_strict_mode.py | 42 ++++---- tests/test_pr_enrichment_integration.py | 78 +++++++-------- tests/test_repo_stability.py | 30 +++--- 16 files changed, 318 insertions(+), 322 deletions(-) diff --git a/autorepro/utils/logging.py b/autorepro/utils/logging.py index 4edd1ad..9e65753 100644 --- a/autorepro/utils/logging.py +++ b/autorepro/utils/logging.py @@ -63,9 +63,7 @@ def format(self, record: logging.LogRecord) -> str: return json.dumps(payload, separators=(",", ":")) - def formatTime( - self, record: logging.LogRecord, datefmt: str | None = None - ) -> str: # noqa: N802 + def formatTime(self, record: logging.LogRecord, datefmt: str | None = None) -> str: # noqa: N802 ct = self.converter(record.created) if datefmt: s = time.strftime(datefmt, ct) @@ -97,7 +95,7 @@ def format(self, record: logging.LogRecord) -> str: for key, value in record.__dict__.items(): if key not in reserved and key not in {"message", "asctime"}: try: - extras.append(f"{key}={json.dumps(value, separators=(',',':'))}") + extras.append(f"{key}={json.dumps(value, separators=(',', ':'))}") except Exception: extras.append(f'{key}="{value}"') if record.exc_info: @@ -109,9 +107,7 @@ def format(self, record: logging.LogRecord) -> str: pass return base + (" " + " ".join(extras) if extras else "") - def formatTime( - self, record: logging.LogRecord, datefmt: str | None = None - ) -> str: # noqa: N802 + def formatTime(self, record: logging.LogRecord, datefmt: str | None = None) -> str: # noqa: N802 # ISO8601-ish UTC time with robust msecs handling ct = time.gmtime(record.created) # Safe access to msecs with fallback calculation for compatibility diff --git a/test_env_and_node.py b/test_env_and_node.py index 43e3a1d..4739c4f 100644 --- a/test_env_and_node.py +++ b/test_env_and_node.py @@ -44,9 +44,9 @@ def test_plan_infers_env_presence(): print(f"Return code: {result.returncode}") # Should succeed - assert ( - result.returncode == 0 - ), f"Expected success, got {result.returncode}. STDERR: {result.stderr}" + assert result.returncode == 0, ( + f"Expected success, got {result.returncode}. STDERR: {result.stderr}" + ) # Assert repro.md contains devcontainer: present in Needed Files/Env repro_file = tmp_path / "repro.md" @@ -55,9 +55,9 @@ def test_plan_infers_env_presence(): content = repro_file.read_text() print(f"Content preview: {content[:300]}...") assert "## Needed Files/Env" in content, "Should have Needed Files/Env section" - assert ( - "devcontainer: present" in content - ), "Should indicate devcontainer is present" + assert "devcontainer: present" in content, ( + "Should indicate devcontainer is present" + ) # Show the specific line lines = content.split("\n") @@ -89,9 +89,9 @@ def test_plan_node_keywords(): print(f"Return code: {result.returncode}") # Should succeed - assert ( - result.returncode == 0 - ), f"Expected success, got {result.returncode}. STDERR: {result.stderr}" + assert result.returncode == 0, ( + f"Expected success, got {result.returncode}. STDERR: {result.stderr}" + ) # Assert output contains either npm test -s or npx jest -w=1 repro_file = tmp_path / "repro.md" @@ -99,9 +99,9 @@ def test_plan_node_keywords(): content = repro_file.read_text() print(f"Content preview: {content[:300]}...") - assert ( - "## Candidate Commands" in content - ), "Should have Candidate Commands section" + assert "## Candidate Commands" in content, ( + "Should have Candidate Commands section" + ) # Should contain either npm test -s or npx jest -w=1 has_npm_test = "npm test -s" in content @@ -109,9 +109,9 @@ def test_plan_node_keywords(): print(f"Has npm test -s: {has_npm_test}") print(f"Has npx jest -w=1: {has_npx_jest}") - assert ( - has_npm_test or has_npx_jest - ), f"Should contain either 'npm test -s' or 'npx jest -w=1' in content: {content}" + assert has_npm_test or has_npx_jest, ( + f"Should contain either 'npm test -s' or 'npx jest -w=1' in content: {content}" + ) # Show the commands section lines = content.split("\n") diff --git a/tests/test_cli_smoke.py b/tests/test_cli_smoke.py index aa07070..87fb6c3 100644 --- a/tests/test_cli_smoke.py +++ b/tests/test_cli_smoke.py @@ -407,9 +407,9 @@ def test_all_help_commands_fast(self, python_cmd): check=False, timeout=10, ) - assert ( - result.returncode == 0 - ), f"Command '{cmd}' failed with exit code {result.returncode}" + assert result.returncode == 0, ( + f"Command '{cmd}' failed with exit code {result.returncode}" + ) @pytest.mark.timeout(30) def test_dry_run_commands_fast(self, python_cmd): @@ -428,6 +428,6 @@ def test_dry_run_commands_fast(self, python_cmd): check=False, timeout=15, ) - assert ( - result.returncode == 0 - ), f"Command {cmd_args} failed with exit code {result.returncode}" + assert result.returncode == 0, ( + f"Command {cmd_args} failed with exit code {result.returncode}" + ) diff --git a/tests/test_cli_verbosity.py b/tests/test_cli_verbosity.py index 7f166a3..4c108c2 100644 --- a/tests/test_cli_verbosity.py +++ b/tests/test_cli_verbosity.py @@ -26,9 +26,9 @@ def test_plan_default_verbosity_hides_filter_message(self): assert result.returncode == 0 assert "filtered" not in result.stderr - assert ( - result.stderr.strip() == "" - ), f"Expected empty stderr, got: {result.stderr}" + assert result.stderr.strip() == "", ( + f"Expected empty stderr, got: {result.stderr}" + ) def test_plan_verbose_shows_filter_message(self): """Test -v shows 'filtered N low-score suggestions' message.""" @@ -88,9 +88,9 @@ def test_plan_quiet_hides_filter_message(self): assert result.returncode == 0 assert "filtered" not in result.stderr - assert ( - result.stderr.strip() == "" - ), f"Expected empty stderr, got: {result.stderr}" + assert result.stderr.strip() == "", ( + f"Expected empty stderr, got: {result.stderr}" + ) def test_plan_strict_quiet_shows_error_only(self): """Test --strict + -q shows error message only.""" @@ -181,9 +181,9 @@ def test_quiet_overrides_verbose(self): assert result.returncode == 0 assert "filtered" not in result.stderr - assert ( - result.stderr.strip() == "" - ), f"Expected empty stderr, got: {result.stderr}" + assert result.stderr.strip() == "", ( + f"Expected empty stderr, got: {result.stderr}" + ) def test_scan_quiet_mode(self): """Test scan with -q shows errors only.""" @@ -198,9 +198,9 @@ def test_scan_quiet_mode(self): assert result.returncode == 0 # Should have stdout output (the scan results) but no stderr - assert ( - result.stderr.strip() == "" - ), f"Expected empty stderr, got: {result.stderr}" + assert result.stderr.strip() == "", ( + f"Expected empty stderr, got: {result.stderr}" + ) def test_scan_verbose_mode(self): """Test scan with -v (informational messages).""" diff --git a/tests/test_config_behavioral.py b/tests/test_config_behavioral.py index 8c0bf3a..578b038 100644 --- a/tests/test_config_behavioral.py +++ b/tests/test_config_behavioral.py @@ -150,9 +150,9 @@ def test_detection_weights_applied(self): for pattern, info in WEIGHTED_PATTERNS.items(): kind = info["kind"] expected_weight = config.detection.weights[kind] - assert ( - info["weight"] == expected_weight - ), f"Pattern {pattern} has wrong weight" + assert info["weight"] == expected_weight, ( + f"Pattern {pattern} has wrong weight" + ) for _pattern, info in SOURCE_PATTERNS.items(): if info["kind"] == "source": @@ -260,9 +260,9 @@ def test_config_access_performance(self): # Should be very fast (< 0.1 seconds for 1000 accesses) duration = end_time - start_time - assert ( - duration < 0.1 - ), f"Config access too slow: {duration} seconds for 1000 accesses" + assert duration < 0.1, ( + f"Config access too slow: {duration} seconds for 1000 accesses" + ) def test_scan_performance_not_regressed(self): """Test that scan performance is not significantly worse.""" diff --git a/tests/test_exit_codes_integration.py b/tests/test_exit_codes_integration.py index 1453989..02c17f8 100644 --- a/tests/test_exit_codes_integration.py +++ b/tests/test_exit_codes_integration.py @@ -30,9 +30,9 @@ def test_scan_with_languages_returns_zero(self, tmp_path): returncode, stdout, stderr = run_autorepro_subprocess(["scan"], cwd=tmp_path) - assert ( - returncode == 0 - ), f"Expected exit code 0, got {returncode}. stderr: {stderr}" + assert returncode == 0, ( + f"Expected exit code 0, got {returncode}. stderr: {stderr}" + ) assert "Detected: python" in stdout assert "python -> pyproject.toml" in stdout @@ -41,18 +41,18 @@ def test_scan_with_no_languages_returns_zero(self, tmp_path): # Empty directory - no language markers returncode, stdout, stderr = run_autorepro_subprocess(["scan"], cwd=tmp_path) - assert ( - returncode == 0 - ), f"Expected exit code 0, got {returncode}. stderr: {stderr}" + assert returncode == 0, ( + f"Expected exit code 0, got {returncode}. stderr: {stderr}" + ) assert "No known languages detected." in stdout def test_init_first_time_returns_zero(self, tmp_path): """Test that init returns 0 on successful creation.""" returncode, stdout, stderr = run_autorepro_subprocess(["init"], cwd=tmp_path) - assert ( - returncode == 0 - ), f"Expected exit code 0, got {returncode}. stderr: {stderr}" + assert returncode == 0, ( + f"Expected exit code 0, got {returncode}. stderr: {stderr}" + ) assert "Wrote devcontainer to" in stdout # Verify file was created @@ -67,9 +67,9 @@ def test_init_already_exists_returns_zero(self, tmp_path): # Run again - should be idempotent success returncode, stdout, stderr = run_autorepro_subprocess(["init"], cwd=tmp_path) - assert ( - returncode == 0 - ), f"Expected exit code 0, got {returncode}. stderr: {stderr}" + assert returncode == 0, ( + f"Expected exit code 0, got {returncode}. stderr: {stderr}" + ) assert "devcontainer.json already exists" in stdout assert "Use --force to overwrite" in stdout @@ -79,9 +79,9 @@ def test_plan_success_returns_zero(self, tmp_path): ["plan", "--desc", "test issue"], cwd=tmp_path ) - assert ( - returncode == 0 - ), f"Expected exit code 0, got {returncode}. stderr: {stderr}" + assert returncode == 0, ( + f"Expected exit code 0, got {returncode}. stderr: {stderr}" + ) assert "Wrote repro to" in stdout # Verify file was created @@ -94,18 +94,18 @@ def test_plan_stdout_returns_zero(self, tmp_path): ["plan", "--desc", "test issue", "--out", "-"], cwd=tmp_path ) - assert ( - returncode == 0 - ), f"Expected exit code 0, got {returncode}. stderr: {stderr}" + assert returncode == 0, ( + f"Expected exit code 0, got {returncode}. stderr: {stderr}" + ) assert "# Test Issue" in stdout # Should contain markdown output def test_help_returns_zero(self, tmp_path): """Test that --help returns 0.""" returncode, stdout, stderr = run_autorepro_subprocess(["--help"], cwd=tmp_path) - assert ( - returncode == 0 - ), f"Expected exit code 0, got {returncode}. stderr: {stderr}" + assert returncode == 0, ( + f"Expected exit code 0, got {returncode}. stderr: {stderr}" + ) assert "CLI for AutoRepro" in stdout @@ -116,9 +116,9 @@ def test_plan_missing_desc_returns_two(self, tmp_path): """Test that plan without --desc returns 2.""" returncode, stdout, stderr = run_autorepro_subprocess(["plan"], cwd=tmp_path) - assert ( - returncode == 2 - ), f"Expected exit code 2, got {returncode}. stdout: {stdout}" + assert returncode == 2, ( + f"Expected exit code 2, got {returncode}. stdout: {stdout}" + ) assert "one of the arguments --desc --file is required" in stderr def test_plan_desc_and_file_returns_two(self, tmp_path): @@ -131,9 +131,9 @@ def test_plan_desc_and_file_returns_two(self, tmp_path): ["plan", "--desc", "test", "--file", str(test_file)], cwd=tmp_path ) - assert ( - returncode == 2 - ), f"Expected exit code 2, got {returncode}. stdout: {stdout}" + assert returncode == 2, ( + f"Expected exit code 2, got {returncode}. stdout: {stdout}" + ) assert "not allowed with argument" in stderr def test_invalid_command_returns_two(self, tmp_path): @@ -142,9 +142,9 @@ def test_invalid_command_returns_two(self, tmp_path): ["invalid_command"], cwd=tmp_path ) - assert ( - returncode == 2 - ), f"Expected exit code 2, got {returncode}. stdout: {stdout}" + assert returncode == 2, ( + f"Expected exit code 2, got {returncode}. stdout: {stdout}" + ) assert "invalid choice: 'invalid_command'" in stderr def test_invalid_repo_path_returns_two(self, tmp_path): @@ -153,9 +153,9 @@ def test_invalid_repo_path_returns_two(self, tmp_path): ["plan", "--desc", "test", "--repo", "/nonexistent/path"], cwd=tmp_path ) - assert ( - returncode == 2 - ), f"Expected exit code 2, got {returncode}. stdout: {stdout}" + assert returncode == 2, ( + f"Expected exit code 2, got {returncode}. stdout: {stdout}" + ) assert "does not exist or is not a directory" in stderr def test_out_points_to_directory_returns_two(self, tmp_path): @@ -168,9 +168,9 @@ def test_out_points_to_directory_returns_two(self, tmp_path): ["plan", "--desc", "test", "--out", str(out_dir)], cwd=tmp_path ) - assert ( - returncode == 2 - ), f"Expected exit code 2, got {returncode}. stdout: {stdout}" + assert returncode == 2, ( + f"Expected exit code 2, got {returncode}. stdout: {stdout}" + ) assert "Output path is a directory" in stdout def test_init_out_points_to_directory_returns_two(self, tmp_path): @@ -183,9 +183,9 @@ def test_init_out_points_to_directory_returns_two(self, tmp_path): ["init", "--out", str(out_dir)], cwd=tmp_path ) - assert ( - returncode == 2 - ), f"Expected exit code 2, got {returncode}. stdout: {stdout}" + assert returncode == 2, ( + f"Expected exit code 2, got {returncode}. stdout: {stdout}" + ) assert "Output path is a directory" in stdout @@ -200,9 +200,9 @@ def test_plan_file_nonexistent_returns_one(self, tmp_path): ["plan", "--file", str(nonexistent_file)], cwd=tmp_path ) - assert ( - returncode == 1 - ), f"Expected exit code 1, got {returncode}. stdout: {stdout}" + assert returncode == 1, ( + f"Expected exit code 1, got {returncode}. stdout: {stdout}" + ) assert f"Error reading file {nonexistent_file}" in stderr def test_plan_file_permission_denied_returns_one(self, tmp_path): @@ -220,9 +220,9 @@ def test_plan_file_permission_denied_returns_one(self, tmp_path): ["plan", "--file", str(restricted_file)], cwd=tmp_path ) - assert ( - returncode == 1 - ), f"Expected exit code 1, got {returncode}. stdout: {stdout}" + assert returncode == 1, ( + f"Expected exit code 1, got {returncode}. stdout: {stdout}" + ) assert f"Error reading file {restricted_file}" in stderr finally: # Restore permissions for cleanup @@ -245,9 +245,9 @@ def test_write_permission_denied_returns_one(self, tmp_path): ["plan", "--desc", "test", "--out", str(output_file)], cwd=tmp_path ) - assert ( - returncode == 1 - ), f"Expected exit code 1, got {returncode}. stdout: {stdout}" + assert returncode == 1, ( + f"Expected exit code 1, got {returncode}. stdout: {stdout}" + ) assert f"Error writing file {output_file}" in stderr finally: # Restore permissions for cleanup diff --git a/tests/test_file_path_resolution.py b/tests/test_file_path_resolution.py index 5862708..c33b698 100644 --- a/tests/test_file_path_resolution.py +++ b/tests/test_file_path_resolution.py @@ -36,9 +36,9 @@ def test_file_relative_to_cwd_success(self, tmp_path): ["--file", "issue.txt", "--repo", str(repo_dir), "--dry-run"], cwd=tmp_path ) - assert ( - result.returncode == 0 - ), f"Expected success, got {result.returncode}. stderr: {result.stderr}" + assert result.returncode == 0, ( + f"Expected success, got {result.returncode}. stderr: {result.stderr}" + ) assert "# Pytest Failing On Ci" in result.stdout assert "pytest" in result.stdout # Should suggest pytest commands @@ -62,9 +62,9 @@ def test_file_fallback_to_repo_success(self, tmp_path): ["--file", "issue.txt", "--repo", str(repo_dir), "--dry-run"], cwd=work_dir ) - assert ( - result.returncode == 0 - ), f"Expected success, got {result.returncode}. stderr: {result.stderr}" + assert result.returncode == 0, ( + f"Expected success, got {result.returncode}. stderr: {result.stderr}" + ) assert "# Go Test Timeout" in result.stdout assert "go test" in result.stdout # Should suggest go test commands @@ -90,9 +90,9 @@ def test_file_cwd_takes_precedence_over_repo(self, tmp_path): ["--file", "issue.txt", "--repo", str(repo_dir), "--dry-run"], cwd=work_dir ) - assert ( - result.returncode == 0 - ), f"Expected success, got {result.returncode}. stderr: {result.stderr}" + assert result.returncode == 0, ( + f"Expected success, got {result.returncode}. stderr: {result.stderr}" + ) assert "# Cwd File Content - Pytest Failing" in result.stdout assert "pytest" in result.stdout # Should suggest pytest (from CWD file) assert "go test" not in result.stdout # Should NOT suggest go (from repo file) @@ -118,9 +118,9 @@ def test_absolute_file_path_ignores_repo(self, tmp_path): cwd=repo_dir, ) - assert ( - result.returncode == 0 - ), f"Expected success, got {result.returncode}. stderr: {result.stderr}" + assert result.returncode == 0, ( + f"Expected success, got {result.returncode}. stderr: {result.stderr}" + ) assert "# Npm Test Failing" in result.stdout def test_file_not_found_anywhere_returns_error(self, tmp_path): @@ -141,9 +141,9 @@ def test_file_not_found_anywhere_returns_error(self, tmp_path): ["--file", "nonexistent.txt", "--repo", str(repo_dir)], cwd=work_dir ) - assert ( - result.returncode == 1 - ), f"Expected I/O error (1), got {result.returncode}. stdout: {result.stdout}" + assert result.returncode == 1, ( + f"Expected I/O error (1), got {result.returncode}. stdout: {result.stdout}" + ) assert "Error reading file" in result.stderr assert "nonexistent.txt" in result.stderr @@ -156,9 +156,9 @@ def test_file_without_repo_uses_cwd_only(self, tmp_path): # Run without --repo - should find file in CWD result = run_plan_subprocess(["--file", "issue.txt", "--dry-run"], cwd=tmp_path) - assert ( - result.returncode == 0 - ), f"Expected success, got {result.returncode}. stderr: {result.stderr}" + assert result.returncode == 0, ( + f"Expected success, got {result.returncode}. stderr: {result.stderr}" + ) assert "# Jest Failing" in result.stdout def test_subdir_file_path_with_repo_fallback(self, tmp_path): @@ -185,7 +185,7 @@ def test_subdir_file_path_with_repo_fallback(self, tmp_path): cwd=work_dir, ) - assert ( - result.returncode == 0 - ), f"Expected success, got {result.returncode}. stderr: {result.stderr}" + assert result.returncode == 0, ( + f"Expected success, got {result.returncode}. stderr: {result.stderr}" + ) assert "# Docker Build Failing" in result.stdout diff --git a/tests/test_focused_implementation.py b/tests/test_focused_implementation.py index d711ffe..7cee308 100644 --- a/tests/test_focused_implementation.py +++ b/tests/test_focused_implementation.py @@ -122,15 +122,15 @@ def test_plan_max_limits_commands(self, tmp_path): print(f"Extracted commands: {limited_commands}") # Assertions - assert ( - len(limited_commands) == 3 - ), f"Expected 3 commands, got {len(limited_commands)}: {limited_commands}" - assert ( - len(full_commands) >= 3 - ), f"Need at least 3 full commands, got {len(full_commands)}: {full_commands}" - assert ( - limited_commands == full_commands[:3] - ), f"Order not preserved: {limited_commands} vs {full_commands[:3]}" + assert len(limited_commands) == 3, ( + f"Expected 3 commands, got {len(limited_commands)}: {limited_commands}" + ) + assert len(full_commands) >= 3, ( + f"Need at least 3 full commands, got {len(full_commands)}: {full_commands}" + ) + assert limited_commands == full_commands[:3], ( + f"Order not preserved: {limited_commands} vs {full_commands[:3]}" + ) class TestInitForceMtimePreservation: @@ -158,9 +158,9 @@ def test_init_force_no_changes_preserves_mtime(self, tmp_path): # mtime should be preserved mtime_after = devcontainer_file.stat().st_mtime - assert ( - mtime_before == mtime_after - ), f"mtime changed: {mtime_before} -> {mtime_after}" + assert mtime_before == mtime_after, ( + f"mtime changed: {mtime_before} -> {mtime_after}" + ) class TestRepoPathStability: @@ -240,9 +240,9 @@ def test_repo_no_cwd_leakage(self, tmp_path): repo_file = repo_dir / "repro.md" current_file = tmp_path / "repro.md" assert repo_file.exists(), "File not created in repo directory" - assert ( - not current_file.exists() - ), "File incorrectly created in current directory" + assert not current_file.exists(), ( + "File incorrectly created in current directory" + ) class TestOutputFilesEndWithNewline: @@ -364,9 +364,9 @@ def test_keyword_match_shows_command(self, tmp_path): and not line.startswith("#") ): command_lines.append(line) - assert ( - len(command_lines) > 0 - ), f"Expected commands due to pytest keyword match, got: {command_lines}" + assert len(command_lines) > 0, ( + f"Expected commands due to pytest keyword match, got: {command_lines}" + ) # At least one should be pytest related pytest_commands = [line for line in command_lines if "pytest" in line] @@ -404,9 +404,9 @@ def test_language_detection_shows_command(self, tmp_path): and not line.startswith("#") ): command_lines.append(line) - assert ( - len(command_lines) > 0 - ), f"Expected commands due to Python language detection, got: {command_lines}" + assert len(command_lines) > 0, ( + f"Expected commands due to Python language detection, got: {command_lines}" + ) # Should have Python-related commands python_commands = [ @@ -414,9 +414,9 @@ def test_language_detection_shows_command(self, tmp_path): for line in command_lines if any(py in line for py in ["python", "pytest"]) ] - assert ( - len(python_commands) > 0 - ), "Expected Python commands due to language detection" + assert len(python_commands) > 0, ( + "Expected Python commands due to language detection" + ) class TestIntegrationExitCodes: @@ -434,30 +434,30 @@ def test_success_commands_return_zero(self, tmp_path): # Test init --out - success result = run_cli_subprocess(["init", "--out", "-"], cwd=tmp_path) - assert ( - result.returncode == 0 - ), f"init --out - should return 0, got {result.returncode}" + assert result.returncode == 0, ( + f"init --out - should return 0, got {result.returncode}" + ) # Test plan --out - success result = run_cli_subprocess( ["plan", "--desc", "test", "--out", "-"], cwd=tmp_path ) - assert ( - result.returncode == 0 - ), f"plan --out - should return 0, got {result.returncode}" + assert result.returncode == 0, ( + f"plan --out - should return 0, got {result.returncode}" + ) def test_misuse_commands_return_two(self, tmp_path): """Test that CLI misuse returns exit code 2.""" # Test plan without required --desc result = run_cli_subprocess(["plan"], cwd=tmp_path) - assert ( - result.returncode == 2 - ), f"plan without --desc should return 2, got {result.returncode}" + assert result.returncode == 2, ( + f"plan without --desc should return 2, got {result.returncode}" + ) # Test invalid --repo path result = run_cli_subprocess( ["plan", "--desc", "test", "--repo", "/nonexistent/path"], cwd=tmp_path ) - assert ( - result.returncode == 2 - ), f"plan with invalid --repo should return 2, got {result.returncode}" + assert result.returncode == 2, ( + f"plan with invalid --repo should return 2, got {result.returncode}" + ) diff --git a/tests/test_golden_plan.py b/tests/test_golden_plan.py index fa70079..15eb668 100644 --- a/tests/test_golden_plan.py +++ b/tests/test_golden_plan.py @@ -117,9 +117,9 @@ def test_plan_jest_watch_assertions(): has_relevant = any( word in first_cmd.lower() for word in ["jest", "npm test", "vitest"] ) - assert ( - has_relevant - ), f"First command should reference jest/npm test/vitest: {first_cmd}" + assert has_relevant, ( + f"First command should reference jest/npm test/vitest: {first_cmd}" + ) def test_plan_ambiguous_assertions(): @@ -135,9 +135,9 @@ def test_plan_ambiguous_assertions(): for line in expected_md.split("\n") if " — " in line and not line.startswith("#") ] - assert ( - len(command_lines) <= 3 - ), f"Should have ≤ 3 commands, got {len(command_lines)}" + assert len(command_lines) <= 3, ( + f"Should have ≤ 3 commands, got {len(command_lines)}" + ) # Test JSON structure expected_json_str = read(GOLDEN_DIR / "plan" / "ambiguous.expected.json") diff --git a/tests/test_init.py b/tests/test_init.py index 077c6f8..00f62bf 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -432,9 +432,9 @@ def test_init_force_no_changes_preserves_mtime(self, tmp_path): # mtime should be unchanged since content is identical mtime2 = os.path.getmtime(devcontainer_file) - assert ( - mtime2 == mtime1 - ), f"mtime should be preserved when content is unchanged. mtime1={mtime1}, mtime2={mtime2}" + assert mtime2 == mtime1, ( + f"mtime should be preserved when content is unchanged. mtime1={mtime1}, mtime2={mtime2}" + ) def test_init_dry_run_ignores_force_flag(self, tmp_path): """Test that --dry-run ignores --force flag and outputs to stdout.""" @@ -497,6 +497,6 @@ def test_init_force_no_changes_preserves_mtime_alt(self, tmp_path): # mtime should be unchanged since content is identical mtime2 = os.path.getmtime(devcontainer_file) - assert ( - mtime2 == mtime1 - ), f"mtime should be preserved when content is unchanged. mtime1={mtime1}, mtime2={mtime2}" + assert mtime2 == mtime1, ( + f"mtime should be preserved when content is unchanged. mtime1={mtime1}, mtime2={mtime2}" + ) diff --git a/tests/test_plan_cli.py b/tests/test_plan_cli.py index bf440ae..e328ce0 100644 --- a/tests/test_plan_cli.py +++ b/tests/test_plan_cli.py @@ -135,9 +135,9 @@ def test_plan_requires_input(self, tmp_path, monkeypatch, capsys): # Check that error message mentions one of --desc/--file is required error_output = result.stderr - assert ( - "one of the arguments --desc --file is required" in error_output - ), f"Expected missing argument error, got: {error_output}" + assert "one of the arguments --desc --file is required" in error_output, ( + f"Expected missing argument error, got: {error_output}" + ) def test_plan_writes_md_default_path(self, tmp_path): """Test that plan writes to repro.md by default and contains expected @@ -149,9 +149,9 @@ def test_plan_writes_md_default_path(self, tmp_path): result = run_cli(tmp_path, "--desc", "pytest failing") # Should succeed - assert ( - result.returncode == 0 - ), f"Expected success, got {result.returncode}. STDERR: {result.stderr}" + assert result.returncode == 0, ( + f"Expected success, got {result.returncode}. STDERR: {result.stderr}" + ) # Assert repro.md exists repro_file = tmp_path / "repro.md" @@ -159,9 +159,9 @@ def test_plan_writes_md_default_path(self, tmp_path): # Assert content contains pytest -q in Candidate Commands content = repro_file.read_text() - assert ( - "## Candidate Commands" in content - ), "Should have Candidate Commands section" + assert "## Candidate Commands" in content, ( + "Should have Candidate Commands section" + ) assert "pytest -q" in content, "Should contain pytest -q command" def test_plan_respects_out_and_force(self, tmp_path): @@ -177,9 +177,9 @@ def test_plan_respects_out_and_force(self, tmp_path): result = run_cli(tmp_path, "--desc", "pytest failing", "--out", str(out_path)) # Should succeed and write to docs/ directory - assert ( - result.returncode == 0 - ), f"Expected success, got {result.returncode}. STDERR: {result.stderr}" + assert result.returncode == 0, ( + f"Expected success, got {result.returncode}. STDERR: {result.stderr}" + ) assert docs_dir.exists(), "docs/ directory should exist" assert out_path.exists(), "repro.md should be created in docs/" @@ -194,9 +194,9 @@ def test_plan_respects_out_and_force(self, tmp_path): # Should succeed but not overwrite assert result2.returncode == 0, f"Expected success, got {result2.returncode}" - assert ( - "exists; use --force to overwrite" in result2.stdout - ), "Should warn about existing file" + assert "exists; use --force to overwrite" in result2.stdout, ( + "Should warn about existing file" + ) # mtime should be unchanged mtime_unchanged = os.path.getmtime(out_path) @@ -211,15 +211,15 @@ def test_plan_respects_out_and_force(self, tmp_path): ) # Should succeed and overwrite - assert ( - result3.returncode == 0 - ), f"Expected success, got {result3.returncode}. STDERR: {result3.stderr}" + assert result3.returncode == 0, ( + f"Expected success, got {result3.returncode}. STDERR: {result3.stderr}" + ) # mtime2 should be greater than mtime1 mtime2 = os.path.getmtime(out_path) - assert ( - mtime2 > mtime1 - ), f"File should be modified with --force. mtime1={mtime1}, mtime2={mtime2}" + assert mtime2 > mtime1, ( + f"File should be modified with --force. mtime1={mtime1}, mtime2={mtime2}" + ) def test_plan_infers_env_presence(self, tmp_path): """Test that plan includes Needed Files/Env section and environment @@ -233,9 +233,9 @@ def test_plan_infers_env_presence(self, tmp_path): result = run_cli(tmp_path, "--desc", "anything") # Should succeed - assert ( - result.returncode == 0 - ), f"Expected success, got {result.returncode}. STDERR: {result.stderr}" + assert result.returncode == 0, ( + f"Expected success, got {result.returncode}. STDERR: {result.stderr}" + ) # Assert repro.md contains Needed Files/Env section repro_file = tmp_path / "repro.md" @@ -254,9 +254,9 @@ def test_plan_infers_env_presence(self, tmp_path): elif env_section_found and line.strip().startswith("- "): has_env_content = True break - assert ( - has_env_content - ), "Should have environment content in Needed Files/Env section" + assert has_env_content, ( + "Should have environment content in Needed Files/Env section" + ) def test_plan_node_keywords(self, tmp_path): """Test that plan detects Node keywords and suggests appropriate commands.""" @@ -268,25 +268,25 @@ def test_plan_node_keywords(self, tmp_path): result = run_cli(tmp_path, "--desc", "tests failing on jest") # Should succeed - assert ( - result.returncode == 0 - ), f"Expected success, got {result.returncode}. STDERR: {result.stderr}" + assert result.returncode == 0, ( + f"Expected success, got {result.returncode}. STDERR: {result.stderr}" + ) # Assert output contains either npm test -s or npx jest -w=1 repro_file = tmp_path / "repro.md" assert repro_file.exists(), "repro.md should be created" content = repro_file.read_text() - assert ( - "## Candidate Commands" in content - ), "Should have Candidate Commands section" + assert "## Candidate Commands" in content, ( + "Should have Candidate Commands section" + ) # Should contain either npm test -s or npx jest -w=1 has_npm_test = "npm test -s" in content has_npx_jest = "npx jest -w=1" in content - assert ( - has_npm_test or has_npx_jest - ), f"Should contain either 'npm test -s' or 'npx jest -w=1' in content: {content}" + assert has_npm_test or has_npx_jest, ( + f"Should contain either 'npm test -s' or 'npx jest -w=1' in content: {content}" + ) class TestPlanCLIBasicFunctionality: @@ -648,14 +648,14 @@ def test_max_option_command_ordering_and_counting(self, tmp_path): ] # Verify command count is limited to 3 - assert ( - len(limited_commands) == 3 - ), f"Expected 3 commands, got {len(limited_commands)}: {limited_commands}" + assert len(limited_commands) == 3, ( + f"Expected 3 commands, got {len(limited_commands)}: {limited_commands}" + ) # Verify that limited commands are the first 3 from the full list (proper ordering) - assert ( - len(full_commands) >= 3 - ), f"Need at least 3 commands in full list, got {len(full_commands)}" + assert len(full_commands) >= 3, ( + f"Need at least 3 commands in full list, got {len(full_commands)}" + ) assert limited_commands == full_commands[:3], ( f"Limited should be first 3 of full list.\n" f"Limited: {limited_commands}\nFull[:3]: {full_commands[:3]}" @@ -727,9 +727,9 @@ def test_json_format_output(self, tmp_path, monkeypatch): # Should contain pytest command due to Python detection and keyword commands = [cmd["cmd"] for cmd in data["commands"]] pytest_commands = [cmd for cmd in commands if "pytest" in cmd] - assert ( - len(pytest_commands) > 0 - ), f"Should include pytest commands. Commands: {commands}" + assert len(pytest_commands) > 0, ( + f"Should include pytest commands. Commands: {commands}" + ) def test_json_format_stdout(self, tmp_path): """Test --format json --out - produces JSON to stdout.""" @@ -918,9 +918,9 @@ def test_ambiguous_case_shows_relevant_commands(self, tmp_path): for line in command_lines ] python_commands = [cmd for cmd in commands if "pytest" in cmd] - assert ( - len(python_commands) > 0 - ), f"Should include pytest commands for Python project. Commands: {commands}" + assert len(python_commands) > 0, ( + f"Should include pytest commands for Python project. Commands: {commands}" + ) def test_keyword_match_without_language_detection(self, tmp_path): """Test that specific keywords show relevant commands even without language @@ -950,9 +950,9 @@ def test_keyword_match_without_language_detection(self, tmp_path): ] # Should include npm test or jest commands node_commands = [cmd for cmd in commands if "npm test" in cmd or "jest" in cmd] - assert ( - len(node_commands) > 0 - ), f"Should include npm/jest commands based on keywords. Commands: {commands}" + assert len(node_commands) > 0, ( + f"Should include npm/jest commands based on keywords. Commands: {commands}" + ) def test_no_matches_shows_no_commands(self, tmp_path): """Test that when no keywords or languages match, no commands are shown.""" @@ -973,9 +973,9 @@ def test_no_matches_shows_no_commands(self, tmp_path): ] # Should show NO commands when no keyword or language matches - assert ( - not command_lines - ), f"Should show no commands when no matches. Got: {command_lines}" + assert not command_lines, ( + f"Should show no commands when no matches. Got: {command_lines}" + ) def test_plan_dry_run_ignores_force_flag(self, tmp_path): """Test that --dry-run ignores --force flag and outputs to stdout.""" @@ -1029,9 +1029,9 @@ def test_ambiguous_case_shows_relevant_commands(self, tmp_path): for line in command_lines ] python_commands = [cmd for cmd in commands if "pytest" in cmd] - assert ( - len(python_commands) > 0 - ), f"Should include pytest commands for Python project. Commands: {commands}" + assert len(python_commands) > 0, ( + f"Should include pytest commands for Python project. Commands: {commands}" + ) def test_keyword_match_without_language_detection(self, tmp_path): """Test that specific keywords show relevant commands even without language @@ -1061,9 +1061,9 @@ def test_keyword_match_without_language_detection(self, tmp_path): ] # Should include npm test or jest commands node_commands = [cmd for cmd in commands if "npm test" in cmd or "jest" in cmd] - assert ( - len(node_commands) > 0 - ), f"Should include npm/jest commands based on keywords. Commands: {commands}" + assert len(node_commands) > 0, ( + f"Should include npm/jest commands based on keywords. Commands: {commands}" + ) def test_no_matches_shows_no_commands(self, tmp_path): """Test that when no keywords or languages match, no commands are shown.""" @@ -1084,6 +1084,6 @@ def test_no_matches_shows_no_commands(self, tmp_path): ] # Should show NO commands when no keyword or language matches - assert ( - not command_lines - ), f"Should show no commands when no matches. Got: {command_lines}" + assert not command_lines, ( + f"Should show no commands when no matches. Got: {command_lines}" + ) diff --git a/tests/test_plan_core.py b/tests/test_plan_core.py index e8b87ef..4f25c96 100644 --- a/tests/test_plan_core.py +++ b/tests/test_plan_core.py @@ -204,9 +204,9 @@ def test_alphabetical_tie_breaking(self): # Check each score group is alphabetically ordered for score, commands in score_groups.items(): - assert commands == sorted( - commands - ), f"Commands with score {score} not alphabetically ordered" + assert commands == sorted(commands), ( + f"Commands with score {score} not alphabetically ordered" + ) def test_detailed_rationales(self): """Test that rationales show matched keywords and detected langs.""" @@ -270,9 +270,9 @@ def test_suggest_commands_weighting(self): vitest_index = next( i for i, (cmd, _, _) in enumerate(suggestions) if cmd == "npx vitest run" ) - assert ( - pytest_index < vitest_index - ), "pytest -q should appear before npx vitest run in sorted results" + assert pytest_index < vitest_index, ( + "pytest -q should appear before npx vitest run in sorted results" + ) class TestSafeTruncate60: @@ -507,19 +507,19 @@ def test_build_repro_md_structure(self): # Assert sections are in correct order expected_order = ["title", "assumptions", "commands", "needs", "next_steps"] actual_order = sorted(section_indices.keys(), key=lambda k: section_indices[k]) - assert ( - actual_order == expected_order - ), f"Sections not in correct order. Expected {expected_order}, got {actual_order}" + assert actual_order == expected_order, ( + f"Sections not in correct order. Expected {expected_order}, got {actual_order}" + ) # Verify section content makes sense assert "Test Issue Title" in lines[section_indices["title"]] - assert any( - "Test assumption" in line for line in lines - ), "Assumption content not found" + assert any("Test assumption" in line for line in lines), ( + "Assumption content not found" + ) assert any("test-cmd" in line for line in lines), "Command content not found" - assert any( - "Test requirement" in line for line in lines - ), "Need content not found" + assert any("Test requirement" in line for line in lines), ( + "Need content not found" + ) assert any("Test step" in line for line in lines), "Next step content not found" diff --git a/tests/test_plan_json_cli.py b/tests/test_plan_json_cli.py index f8af1a2..2dc5b4e 100644 --- a/tests/test_plan_json_cli.py +++ b/tests/test_plan_json_cli.py @@ -442,9 +442,9 @@ def test_json_stable_command_order(self, tmp_path): # Check scores are in descending order or tied for i in range(1, len(scores)): - assert ( - scores[i] <= scores[i - 1] - ), f"Scores not in descending order: {scores}" + assert scores[i] <= scores[i - 1], ( + f"Scores not in descending order: {scores}" + ) # Within same score, should be alphabetical score_groups = {} @@ -455,6 +455,6 @@ def test_json_stable_command_order(self, tmp_path): score_groups[score].append(cmd["cmd"]) for score, cmds in score_groups.items(): - assert cmds == sorted( - cmds - ), f"Commands with score {score} not alphabetical: {cmds}" + assert cmds == sorted(cmds), ( + f"Commands with score {score} not alphabetical: {cmds}" + ) diff --git a/tests/test_plan_strict_mode.py b/tests/test_plan_strict_mode.py index 661390a..3b6adac 100644 --- a/tests/test_plan_strict_mode.py +++ b/tests/test_plan_strict_mode.py @@ -58,9 +58,9 @@ def test_min_score_filters_low_weight_commands(self): # Test with default min_score=2 - should include Python commands suggestions_default = suggest_commands(keywords, detected_langs, min_score=2) - assert ( - len(suggestions_default) > 0 - ), "Should include Python commands with score >= 2" + assert len(suggestions_default) > 0, ( + "Should include Python commands with score >= 2" + ) # Test with high min_score=4 - should exclude Python-only commands (score 2) suggestions_high = suggest_commands(keywords, detected_langs, min_score=4) @@ -68,9 +68,9 @@ def test_min_score_filters_low_weight_commands(self): # Test with low min_score=1 - should include more commands suggestions_low = suggest_commands(keywords, detected_langs, min_score=1) - assert len(suggestions_low) >= len( - suggestions_default - ), "Lower min_score should include more commands" + assert len(suggestions_low) >= len(suggestions_default), ( + "Lower min_score should include more commands" + ) def test_keyword_match_respects_min_score(self): """Test that direct keyword matches are filtered by min_score.""" @@ -81,9 +81,9 @@ def test_keyword_match_respects_min_score(self): # With min_score=3, should include basic pytest commands suggestions_low = suggest_commands(keywords, detected_langs, min_score=3) - assert ( - len(suggestions_low) > 0 - ), "Should include commands with score >= min_score" + assert len(suggestions_low) > 0, ( + "Should include commands with score >= min_score" + ) # With min_score=5, should exclude all pytest commands (max score is 4) suggestions_high = suggest_commands(keywords, detected_langs, min_score=5) @@ -98,9 +98,9 @@ def test_language_match_respects_min_score(self): # With min_score=2, should include python commands suggestions_low = suggest_commands(keywords, detected_langs, min_score=2) - assert ( - len(suggestions_low) > 0 - ), "Should include commands with score >= min_score" + assert len(suggestions_low) > 0, ( + "Should include commands with score >= min_score" + ) # With min_score=4, should exclude python commands (max score is 3) suggestions_high = suggest_commands(keywords, detected_langs, min_score=4) @@ -129,9 +129,9 @@ def test_strict_mode_exit_0_when_commands_exist(self, tmp_path): ["--desc", "pytest failing", "--min-score", "2", "--strict"], cwd=tmp_path ) - assert ( - result.returncode == 0 - ), f"Expected exit code 0, got {result.returncode}. STDERR: {result.stderr}" + assert result.returncode == 0, ( + f"Expected exit code 0, got {result.returncode}. STDERR: {result.stderr}" + ) def test_non_strict_mode_always_exit_0(self, tmp_path): """Test non-strict mode always exits 0 even with no commands.""" @@ -140,9 +140,9 @@ def test_non_strict_mode_always_exit_0(self, tmp_path): ["--desc", "random generic issue", "--min-score", "5"], cwd=tmp_path ) - assert ( - result.returncode == 0 - ), f"Non-strict mode should always exit 0, got {result.returncode}" + assert result.returncode == 0, ( + f"Non-strict mode should always exit 0, got {result.returncode}" + ) class TestPlanCLIMinScore: @@ -195,9 +195,9 @@ def test_min_score_filters_output(self, tmp_path): ] ) - assert ( - low_commands >= high_commands - ), "Lower min-score should show same or more commands" + assert low_commands >= high_commands, ( + "Lower min-score should show same or more commands" + ) def test_filtering_warning_message(self, tmp_path): """Test that filtering warning message is printed to stderr.""" diff --git a/tests/test_pr_enrichment_integration.py b/tests/test_pr_enrichment_integration.py index d40be4e..5240f3f 100644 --- a/tests/test_pr_enrichment_integration.py +++ b/tests/test_pr_enrichment_integration.py @@ -188,9 +188,9 @@ def test_pr_comment_create_new(self, fake_env_setup): ) # Verify success - assert ( - result.returncode == 0 - ), f"stdout: {result.stdout}\nstderr: {result.stderr}" + assert result.returncode == 0, ( + f"stdout: {result.stdout}\nstderr: {result.stderr}" + ) # Should create PR comment with sync block assert "Created autorepro comment" in result.stderr @@ -232,9 +232,9 @@ def test_pr_comment_update_existing(self, fake_env_setup): ) # Verify success and that existing comment was updated - assert ( - result.returncode == 0 - ), f"stdout: {result.stdout}\nstderr: {result.stderr}" + assert result.returncode == 0, ( + f"stdout: {result.stdout}\nstderr: {result.stderr}" + ) assert ( "Updated autorepro comment" in result.stderr or "Created autorepro comment" in result.stderr @@ -278,9 +278,9 @@ def test_pr_body_update_sync_block(self, fake_env_setup): ) # Verify success - assert ( - result.returncode == 0 - ), f"stdout: {result.stdout}\nstderr: {result.stderr}" + assert result.returncode == 0, ( + f"stdout: {result.stdout}\nstderr: {result.stderr}" + ) # Should update PR body with sync block assert ( "Updated sync block" in result.stderr @@ -325,9 +325,9 @@ def test_pr_body_update_existing_sync_block(self, fake_env_setup): ) # Verify success and existing sync block was replaced - assert ( - result.returncode == 0 - ), f"stdout: {result.stdout}\nstderr: {result.stderr}" + assert result.returncode == 0, ( + f"stdout: {result.stdout}\nstderr: {result.stderr}" + ) assert ( "Updated sync block" in result.stderr or "Replacing existing sync block" in result.stderr @@ -372,9 +372,9 @@ def test_pr_add_labels(self, fake_env_setup): ) # Verify success - assert ( - result.returncode == 0 - ), f"stdout: {result.stdout}\nstderr: {result.stderr}" + assert result.returncode == 0, ( + f"stdout: {result.stdout}\nstderr: {result.stderr}" + ) # Should add labels to PR assert ( "Added labels" in result.stderr or "Updated PR with labels" in result.stderr @@ -419,9 +419,9 @@ def test_pr_link_issue_cross_reference(self, fake_env_setup): ) # Verify success - assert ( - result.returncode == 0 - ), f"stdout: {result.stdout}\nstderr: {result.stderr}" + assert result.returncode == 0, ( + f"stdout: {result.stdout}\nstderr: {result.stderr}" + ) # Should create cross-reference link assert ( "Cross-linked to issue" in result.stderr @@ -467,9 +467,9 @@ def test_pr_attach_report_metadata(self, fake_env_setup): ) # Verify success - assert ( - result.returncode == 0 - ), f"stdout: {result.stdout}\nstderr: {result.stderr}" + assert result.returncode == 0, ( + f"stdout: {result.stdout}\nstderr: {result.stderr}" + ) # Comment should include report metadata assert ( "Created autorepro comment" in result.stderr @@ -516,9 +516,9 @@ def test_pr_summary_context(self, fake_env_setup): ) # Verify success - assert ( - result.returncode == 0 - ), f"stdout: {result.stdout}\nstderr: {result.stderr}" + assert result.returncode == 0, ( + f"stdout: {result.stdout}\nstderr: {result.stderr}" + ) # Comment should include summary context assert ( "Created autorepro comment" in result.stderr @@ -564,9 +564,9 @@ def test_pr_no_details_flag(self, fake_env_setup): ) # Verify success - assert ( - result.returncode == 0 - ), f"stdout: {result.stdout}\nstderr: {result.stderr}" + assert result.returncode == 0, ( + f"stdout: {result.stdout}\nstderr: {result.stderr}" + ) # Comment should be created without details wrapper assert ( "Created autorepro comment" in result.stderr @@ -620,9 +620,9 @@ def test_pr_all_enrichment_features_combined(self, fake_env_setup): ) # Verify success with all features - assert ( - result.returncode == 0 - ), f"stdout: {result.stdout}\nstderr: {result.stderr}" + assert result.returncode == 0, ( + f"stdout: {result.stdout}\nstderr: {result.stderr}" + ) # Should create/update PR comment, update body, add labels, create cross-links stderr_text = result.stderr assert any( @@ -677,9 +677,9 @@ def test_pr_enrichment_dry_run_mode(self, fake_env_setup): ) # Verify dry-run success - assert ( - result.returncode == 0 - ), f"stdout: {result.stdout}\nstderr: {result.stderr}" + assert result.returncode == 0, ( + f"stdout: {result.stdout}\nstderr: {result.stderr}" + ) # Should show what would be done without actually doing it stdout_text = result.stdout assert "Would run: gh pr create" in stdout_text @@ -729,9 +729,9 @@ def test_pr_enrichment_format_json(self, fake_env_setup): ) # Verify success with JSON format - assert ( - result.returncode == 0 - ), f"stdout: {result.stdout}\nstderr: {result.stderr}" + assert result.returncode == 0, ( + f"stdout: {result.stdout}\nstderr: {result.stderr}" + ) # Comment should be created with JSON format assert ( "Created autorepro comment" in result.stderr @@ -847,7 +847,7 @@ def test_pr_enrichment_mutual_exclusions(self, fake_env_setup): ) # This combination should work (both comment and body updates) - assert ( - result.returncode == 0 - ), f"stdout: {result.stdout}\nstderr: {result.stderr}" + assert result.returncode == 0, ( + f"stdout: {result.stdout}\nstderr: {result.stderr}" + ) assert "Would run: gh pr create" in result.stdout diff --git a/tests/test_repo_stability.py b/tests/test_repo_stability.py index 98c6a12..77c598a 100644 --- a/tests/test_repo_stability.py +++ b/tests/test_repo_stability.py @@ -97,12 +97,12 @@ def test_init_repo_path_resolved_consistently(self, tmp_path): # Both should create devcontainer files in their respective repo directories devcontainer1 = repo_dir1 / ".devcontainer" / "devcontainer.json" devcontainer2 = repo_dir2 / ".devcontainer" / "devcontainer.json" - assert ( - devcontainer1.exists() - ), "devcontainer.json should be created in repo_dir1" - assert ( - devcontainer2.exists() - ), "devcontainer.json should be created in repo_dir2" + assert devcontainer1.exists(), ( + "devcontainer.json should be created in repo_dir1" + ) + assert devcontainer2.exists(), ( + "devcontainer.json should be created in repo_dir2" + ) def test_repo_nonexistent_path_exit_2(self, tmp_path): """Test that nonexistent --repo path returns exit code 2.""" @@ -142,9 +142,9 @@ def test_repo_cwd_not_changed(self, tmp_path): repo_plan_file = repo_dir / "repro.md" current_plan_file = tmp_path / "repro.md" assert repo_plan_file.exists(), "Plan should be created in repo directory" - assert ( - not current_plan_file.exists() - ), "Plan should NOT be created in current directory" + assert not current_plan_file.exists(), ( + "Plan should NOT be created in current directory" + ) # Run init command with --repo result2 = run_cli_subprocess(["init", "--repo", str(repo_dir)], cwd=tmp_path) @@ -156,9 +156,9 @@ def test_repo_cwd_not_changed(self, tmp_path): # Devcontainer should be created in repo directory repo_devcontainer = repo_dir / ".devcontainer" / "devcontainer.json" current_devcontainer = tmp_path / ".devcontainer" / "devcontainer.json" - assert ( - repo_devcontainer.exists() - ), "Devcontainer should be created in repo directory" - assert ( - not current_devcontainer.exists() - ), "Devcontainer should NOT be created in current directory" + assert repo_devcontainer.exists(), ( + "Devcontainer should be created in repo directory" + ) + assert not current_devcontainer.exists(), ( + "Devcontainer should NOT be created in current directory" + ) From afa394c6cbf82e86725c0e0b01b0bcf8e8a896e6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Sep 2025 09:19:52 +0000 Subject: [PATCH 7/8] Fix assertion formatting style to comply with pre-commit hooks Co-authored-by: ali90h <89179776+ali90h@users.noreply.github.com> --- autorepro/utils/logging.py | 8 +- test_env_and_node.py | 30 +++--- tests/test_cli_smoke.py | 12 +-- tests/test_cli_verbosity.py | 24 ++--- tests/test_config_behavioral.py | 12 +-- tests/test_exit_codes_integration.py | 96 +++++++++--------- tests/test_file_path_resolution.py | 42 ++++---- tests/test_focused_implementation.py | 72 +++++++------- tests/test_golden_plan.py | 12 +-- tests/test_init.py | 12 +-- tests/test_plan_cli.py | 126 ++++++++++++------------ tests/test_plan_core.py | 30 +++--- tests/test_plan_json_cli.py | 12 +-- tests/test_plan_strict_mode.py | 42 ++++---- tests/test_pr_enrichment_integration.py | 78 +++++++-------- tests/test_repo_stability.py | 30 +++--- 16 files changed, 321 insertions(+), 317 deletions(-) diff --git a/autorepro/utils/logging.py b/autorepro/utils/logging.py index 9e65753..2b51128 100644 --- a/autorepro/utils/logging.py +++ b/autorepro/utils/logging.py @@ -63,7 +63,9 @@ def format(self, record: logging.LogRecord) -> str: return json.dumps(payload, separators=(",", ":")) - def formatTime(self, record: logging.LogRecord, datefmt: str | None = None) -> str: # noqa: N802 + def formatTime( + self, record: logging.LogRecord, datefmt: str | None = None + ) -> str: # noqa: N802 ct = self.converter(record.created) if datefmt: s = time.strftime(datefmt, ct) @@ -107,7 +109,9 @@ def format(self, record: logging.LogRecord) -> str: pass return base + (" " + " ".join(extras) if extras else "") - def formatTime(self, record: logging.LogRecord, datefmt: str | None = None) -> str: # noqa: N802 + def formatTime( + self, record: logging.LogRecord, datefmt: str | None = None + ) -> str: # noqa: N802 # ISO8601-ish UTC time with robust msecs handling ct = time.gmtime(record.created) # Safe access to msecs with fallback calculation for compatibility diff --git a/test_env_and_node.py b/test_env_and_node.py index 4739c4f..43e3a1d 100644 --- a/test_env_and_node.py +++ b/test_env_and_node.py @@ -44,9 +44,9 @@ def test_plan_infers_env_presence(): print(f"Return code: {result.returncode}") # Should succeed - assert result.returncode == 0, ( - f"Expected success, got {result.returncode}. STDERR: {result.stderr}" - ) + assert ( + result.returncode == 0 + ), f"Expected success, got {result.returncode}. STDERR: {result.stderr}" # Assert repro.md contains devcontainer: present in Needed Files/Env repro_file = tmp_path / "repro.md" @@ -55,9 +55,9 @@ def test_plan_infers_env_presence(): content = repro_file.read_text() print(f"Content preview: {content[:300]}...") assert "## Needed Files/Env" in content, "Should have Needed Files/Env section" - assert "devcontainer: present" in content, ( - "Should indicate devcontainer is present" - ) + assert ( + "devcontainer: present" in content + ), "Should indicate devcontainer is present" # Show the specific line lines = content.split("\n") @@ -89,9 +89,9 @@ def test_plan_node_keywords(): print(f"Return code: {result.returncode}") # Should succeed - assert result.returncode == 0, ( - f"Expected success, got {result.returncode}. STDERR: {result.stderr}" - ) + assert ( + result.returncode == 0 + ), f"Expected success, got {result.returncode}. STDERR: {result.stderr}" # Assert output contains either npm test -s or npx jest -w=1 repro_file = tmp_path / "repro.md" @@ -99,9 +99,9 @@ def test_plan_node_keywords(): content = repro_file.read_text() print(f"Content preview: {content[:300]}...") - assert "## Candidate Commands" in content, ( - "Should have Candidate Commands section" - ) + assert ( + "## Candidate Commands" in content + ), "Should have Candidate Commands section" # Should contain either npm test -s or npx jest -w=1 has_npm_test = "npm test -s" in content @@ -109,9 +109,9 @@ def test_plan_node_keywords(): print(f"Has npm test -s: {has_npm_test}") print(f"Has npx jest -w=1: {has_npx_jest}") - assert has_npm_test or has_npx_jest, ( - f"Should contain either 'npm test -s' or 'npx jest -w=1' in content: {content}" - ) + assert ( + has_npm_test or has_npx_jest + ), f"Should contain either 'npm test -s' or 'npx jest -w=1' in content: {content}" # Show the commands section lines = content.split("\n") diff --git a/tests/test_cli_smoke.py b/tests/test_cli_smoke.py index 87fb6c3..aa07070 100644 --- a/tests/test_cli_smoke.py +++ b/tests/test_cli_smoke.py @@ -407,9 +407,9 @@ def test_all_help_commands_fast(self, python_cmd): check=False, timeout=10, ) - assert result.returncode == 0, ( - f"Command '{cmd}' failed with exit code {result.returncode}" - ) + assert ( + result.returncode == 0 + ), f"Command '{cmd}' failed with exit code {result.returncode}" @pytest.mark.timeout(30) def test_dry_run_commands_fast(self, python_cmd): @@ -428,6 +428,6 @@ def test_dry_run_commands_fast(self, python_cmd): check=False, timeout=15, ) - assert result.returncode == 0, ( - f"Command {cmd_args} failed with exit code {result.returncode}" - ) + assert ( + result.returncode == 0 + ), f"Command {cmd_args} failed with exit code {result.returncode}" diff --git a/tests/test_cli_verbosity.py b/tests/test_cli_verbosity.py index 4c108c2..7f166a3 100644 --- a/tests/test_cli_verbosity.py +++ b/tests/test_cli_verbosity.py @@ -26,9 +26,9 @@ def test_plan_default_verbosity_hides_filter_message(self): assert result.returncode == 0 assert "filtered" not in result.stderr - assert result.stderr.strip() == "", ( - f"Expected empty stderr, got: {result.stderr}" - ) + assert ( + result.stderr.strip() == "" + ), f"Expected empty stderr, got: {result.stderr}" def test_plan_verbose_shows_filter_message(self): """Test -v shows 'filtered N low-score suggestions' message.""" @@ -88,9 +88,9 @@ def test_plan_quiet_hides_filter_message(self): assert result.returncode == 0 assert "filtered" not in result.stderr - assert result.stderr.strip() == "", ( - f"Expected empty stderr, got: {result.stderr}" - ) + assert ( + result.stderr.strip() == "" + ), f"Expected empty stderr, got: {result.stderr}" def test_plan_strict_quiet_shows_error_only(self): """Test --strict + -q shows error message only.""" @@ -181,9 +181,9 @@ def test_quiet_overrides_verbose(self): assert result.returncode == 0 assert "filtered" not in result.stderr - assert result.stderr.strip() == "", ( - f"Expected empty stderr, got: {result.stderr}" - ) + assert ( + result.stderr.strip() == "" + ), f"Expected empty stderr, got: {result.stderr}" def test_scan_quiet_mode(self): """Test scan with -q shows errors only.""" @@ -198,9 +198,9 @@ def test_scan_quiet_mode(self): assert result.returncode == 0 # Should have stdout output (the scan results) but no stderr - assert result.stderr.strip() == "", ( - f"Expected empty stderr, got: {result.stderr}" - ) + assert ( + result.stderr.strip() == "" + ), f"Expected empty stderr, got: {result.stderr}" def test_scan_verbose_mode(self): """Test scan with -v (informational messages).""" diff --git a/tests/test_config_behavioral.py b/tests/test_config_behavioral.py index 578b038..8c0bf3a 100644 --- a/tests/test_config_behavioral.py +++ b/tests/test_config_behavioral.py @@ -150,9 +150,9 @@ def test_detection_weights_applied(self): for pattern, info in WEIGHTED_PATTERNS.items(): kind = info["kind"] expected_weight = config.detection.weights[kind] - assert info["weight"] == expected_weight, ( - f"Pattern {pattern} has wrong weight" - ) + assert ( + info["weight"] == expected_weight + ), f"Pattern {pattern} has wrong weight" for _pattern, info in SOURCE_PATTERNS.items(): if info["kind"] == "source": @@ -260,9 +260,9 @@ def test_config_access_performance(self): # Should be very fast (< 0.1 seconds for 1000 accesses) duration = end_time - start_time - assert duration < 0.1, ( - f"Config access too slow: {duration} seconds for 1000 accesses" - ) + assert ( + duration < 0.1 + ), f"Config access too slow: {duration} seconds for 1000 accesses" def test_scan_performance_not_regressed(self): """Test that scan performance is not significantly worse.""" diff --git a/tests/test_exit_codes_integration.py b/tests/test_exit_codes_integration.py index 02c17f8..1453989 100644 --- a/tests/test_exit_codes_integration.py +++ b/tests/test_exit_codes_integration.py @@ -30,9 +30,9 @@ def test_scan_with_languages_returns_zero(self, tmp_path): returncode, stdout, stderr = run_autorepro_subprocess(["scan"], cwd=tmp_path) - assert returncode == 0, ( - f"Expected exit code 0, got {returncode}. stderr: {stderr}" - ) + assert ( + returncode == 0 + ), f"Expected exit code 0, got {returncode}. stderr: {stderr}" assert "Detected: python" in stdout assert "python -> pyproject.toml" in stdout @@ -41,18 +41,18 @@ def test_scan_with_no_languages_returns_zero(self, tmp_path): # Empty directory - no language markers returncode, stdout, stderr = run_autorepro_subprocess(["scan"], cwd=tmp_path) - assert returncode == 0, ( - f"Expected exit code 0, got {returncode}. stderr: {stderr}" - ) + assert ( + returncode == 0 + ), f"Expected exit code 0, got {returncode}. stderr: {stderr}" assert "No known languages detected." in stdout def test_init_first_time_returns_zero(self, tmp_path): """Test that init returns 0 on successful creation.""" returncode, stdout, stderr = run_autorepro_subprocess(["init"], cwd=tmp_path) - assert returncode == 0, ( - f"Expected exit code 0, got {returncode}. stderr: {stderr}" - ) + assert ( + returncode == 0 + ), f"Expected exit code 0, got {returncode}. stderr: {stderr}" assert "Wrote devcontainer to" in stdout # Verify file was created @@ -67,9 +67,9 @@ def test_init_already_exists_returns_zero(self, tmp_path): # Run again - should be idempotent success returncode, stdout, stderr = run_autorepro_subprocess(["init"], cwd=tmp_path) - assert returncode == 0, ( - f"Expected exit code 0, got {returncode}. stderr: {stderr}" - ) + assert ( + returncode == 0 + ), f"Expected exit code 0, got {returncode}. stderr: {stderr}" assert "devcontainer.json already exists" in stdout assert "Use --force to overwrite" in stdout @@ -79,9 +79,9 @@ def test_plan_success_returns_zero(self, tmp_path): ["plan", "--desc", "test issue"], cwd=tmp_path ) - assert returncode == 0, ( - f"Expected exit code 0, got {returncode}. stderr: {stderr}" - ) + assert ( + returncode == 0 + ), f"Expected exit code 0, got {returncode}. stderr: {stderr}" assert "Wrote repro to" in stdout # Verify file was created @@ -94,18 +94,18 @@ def test_plan_stdout_returns_zero(self, tmp_path): ["plan", "--desc", "test issue", "--out", "-"], cwd=tmp_path ) - assert returncode == 0, ( - f"Expected exit code 0, got {returncode}. stderr: {stderr}" - ) + assert ( + returncode == 0 + ), f"Expected exit code 0, got {returncode}. stderr: {stderr}" assert "# Test Issue" in stdout # Should contain markdown output def test_help_returns_zero(self, tmp_path): """Test that --help returns 0.""" returncode, stdout, stderr = run_autorepro_subprocess(["--help"], cwd=tmp_path) - assert returncode == 0, ( - f"Expected exit code 0, got {returncode}. stderr: {stderr}" - ) + assert ( + returncode == 0 + ), f"Expected exit code 0, got {returncode}. stderr: {stderr}" assert "CLI for AutoRepro" in stdout @@ -116,9 +116,9 @@ def test_plan_missing_desc_returns_two(self, tmp_path): """Test that plan without --desc returns 2.""" returncode, stdout, stderr = run_autorepro_subprocess(["plan"], cwd=tmp_path) - assert returncode == 2, ( - f"Expected exit code 2, got {returncode}. stdout: {stdout}" - ) + assert ( + returncode == 2 + ), f"Expected exit code 2, got {returncode}. stdout: {stdout}" assert "one of the arguments --desc --file is required" in stderr def test_plan_desc_and_file_returns_two(self, tmp_path): @@ -131,9 +131,9 @@ def test_plan_desc_and_file_returns_two(self, tmp_path): ["plan", "--desc", "test", "--file", str(test_file)], cwd=tmp_path ) - assert returncode == 2, ( - f"Expected exit code 2, got {returncode}. stdout: {stdout}" - ) + assert ( + returncode == 2 + ), f"Expected exit code 2, got {returncode}. stdout: {stdout}" assert "not allowed with argument" in stderr def test_invalid_command_returns_two(self, tmp_path): @@ -142,9 +142,9 @@ def test_invalid_command_returns_two(self, tmp_path): ["invalid_command"], cwd=tmp_path ) - assert returncode == 2, ( - f"Expected exit code 2, got {returncode}. stdout: {stdout}" - ) + assert ( + returncode == 2 + ), f"Expected exit code 2, got {returncode}. stdout: {stdout}" assert "invalid choice: 'invalid_command'" in stderr def test_invalid_repo_path_returns_two(self, tmp_path): @@ -153,9 +153,9 @@ def test_invalid_repo_path_returns_two(self, tmp_path): ["plan", "--desc", "test", "--repo", "/nonexistent/path"], cwd=tmp_path ) - assert returncode == 2, ( - f"Expected exit code 2, got {returncode}. stdout: {stdout}" - ) + assert ( + returncode == 2 + ), f"Expected exit code 2, got {returncode}. stdout: {stdout}" assert "does not exist or is not a directory" in stderr def test_out_points_to_directory_returns_two(self, tmp_path): @@ -168,9 +168,9 @@ def test_out_points_to_directory_returns_two(self, tmp_path): ["plan", "--desc", "test", "--out", str(out_dir)], cwd=tmp_path ) - assert returncode == 2, ( - f"Expected exit code 2, got {returncode}. stdout: {stdout}" - ) + assert ( + returncode == 2 + ), f"Expected exit code 2, got {returncode}. stdout: {stdout}" assert "Output path is a directory" in stdout def test_init_out_points_to_directory_returns_two(self, tmp_path): @@ -183,9 +183,9 @@ def test_init_out_points_to_directory_returns_two(self, tmp_path): ["init", "--out", str(out_dir)], cwd=tmp_path ) - assert returncode == 2, ( - f"Expected exit code 2, got {returncode}. stdout: {stdout}" - ) + assert ( + returncode == 2 + ), f"Expected exit code 2, got {returncode}. stdout: {stdout}" assert "Output path is a directory" in stdout @@ -200,9 +200,9 @@ def test_plan_file_nonexistent_returns_one(self, tmp_path): ["plan", "--file", str(nonexistent_file)], cwd=tmp_path ) - assert returncode == 1, ( - f"Expected exit code 1, got {returncode}. stdout: {stdout}" - ) + assert ( + returncode == 1 + ), f"Expected exit code 1, got {returncode}. stdout: {stdout}" assert f"Error reading file {nonexistent_file}" in stderr def test_plan_file_permission_denied_returns_one(self, tmp_path): @@ -220,9 +220,9 @@ def test_plan_file_permission_denied_returns_one(self, tmp_path): ["plan", "--file", str(restricted_file)], cwd=tmp_path ) - assert returncode == 1, ( - f"Expected exit code 1, got {returncode}. stdout: {stdout}" - ) + assert ( + returncode == 1 + ), f"Expected exit code 1, got {returncode}. stdout: {stdout}" assert f"Error reading file {restricted_file}" in stderr finally: # Restore permissions for cleanup @@ -245,9 +245,9 @@ def test_write_permission_denied_returns_one(self, tmp_path): ["plan", "--desc", "test", "--out", str(output_file)], cwd=tmp_path ) - assert returncode == 1, ( - f"Expected exit code 1, got {returncode}. stdout: {stdout}" - ) + assert ( + returncode == 1 + ), f"Expected exit code 1, got {returncode}. stdout: {stdout}" assert f"Error writing file {output_file}" in stderr finally: # Restore permissions for cleanup diff --git a/tests/test_file_path_resolution.py b/tests/test_file_path_resolution.py index c33b698..5862708 100644 --- a/tests/test_file_path_resolution.py +++ b/tests/test_file_path_resolution.py @@ -36,9 +36,9 @@ def test_file_relative_to_cwd_success(self, tmp_path): ["--file", "issue.txt", "--repo", str(repo_dir), "--dry-run"], cwd=tmp_path ) - assert result.returncode == 0, ( - f"Expected success, got {result.returncode}. stderr: {result.stderr}" - ) + assert ( + result.returncode == 0 + ), f"Expected success, got {result.returncode}. stderr: {result.stderr}" assert "# Pytest Failing On Ci" in result.stdout assert "pytest" in result.stdout # Should suggest pytest commands @@ -62,9 +62,9 @@ def test_file_fallback_to_repo_success(self, tmp_path): ["--file", "issue.txt", "--repo", str(repo_dir), "--dry-run"], cwd=work_dir ) - assert result.returncode == 0, ( - f"Expected success, got {result.returncode}. stderr: {result.stderr}" - ) + assert ( + result.returncode == 0 + ), f"Expected success, got {result.returncode}. stderr: {result.stderr}" assert "# Go Test Timeout" in result.stdout assert "go test" in result.stdout # Should suggest go test commands @@ -90,9 +90,9 @@ def test_file_cwd_takes_precedence_over_repo(self, tmp_path): ["--file", "issue.txt", "--repo", str(repo_dir), "--dry-run"], cwd=work_dir ) - assert result.returncode == 0, ( - f"Expected success, got {result.returncode}. stderr: {result.stderr}" - ) + assert ( + result.returncode == 0 + ), f"Expected success, got {result.returncode}. stderr: {result.stderr}" assert "# Cwd File Content - Pytest Failing" in result.stdout assert "pytest" in result.stdout # Should suggest pytest (from CWD file) assert "go test" not in result.stdout # Should NOT suggest go (from repo file) @@ -118,9 +118,9 @@ def test_absolute_file_path_ignores_repo(self, tmp_path): cwd=repo_dir, ) - assert result.returncode == 0, ( - f"Expected success, got {result.returncode}. stderr: {result.stderr}" - ) + assert ( + result.returncode == 0 + ), f"Expected success, got {result.returncode}. stderr: {result.stderr}" assert "# Npm Test Failing" in result.stdout def test_file_not_found_anywhere_returns_error(self, tmp_path): @@ -141,9 +141,9 @@ def test_file_not_found_anywhere_returns_error(self, tmp_path): ["--file", "nonexistent.txt", "--repo", str(repo_dir)], cwd=work_dir ) - assert result.returncode == 1, ( - f"Expected I/O error (1), got {result.returncode}. stdout: {result.stdout}" - ) + assert ( + result.returncode == 1 + ), f"Expected I/O error (1), got {result.returncode}. stdout: {result.stdout}" assert "Error reading file" in result.stderr assert "nonexistent.txt" in result.stderr @@ -156,9 +156,9 @@ def test_file_without_repo_uses_cwd_only(self, tmp_path): # Run without --repo - should find file in CWD result = run_plan_subprocess(["--file", "issue.txt", "--dry-run"], cwd=tmp_path) - assert result.returncode == 0, ( - f"Expected success, got {result.returncode}. stderr: {result.stderr}" - ) + assert ( + result.returncode == 0 + ), f"Expected success, got {result.returncode}. stderr: {result.stderr}" assert "# Jest Failing" in result.stdout def test_subdir_file_path_with_repo_fallback(self, tmp_path): @@ -185,7 +185,7 @@ def test_subdir_file_path_with_repo_fallback(self, tmp_path): cwd=work_dir, ) - assert result.returncode == 0, ( - f"Expected success, got {result.returncode}. stderr: {result.stderr}" - ) + assert ( + result.returncode == 0 + ), f"Expected success, got {result.returncode}. stderr: {result.stderr}" assert "# Docker Build Failing" in result.stdout diff --git a/tests/test_focused_implementation.py b/tests/test_focused_implementation.py index 7cee308..d711ffe 100644 --- a/tests/test_focused_implementation.py +++ b/tests/test_focused_implementation.py @@ -122,15 +122,15 @@ def test_plan_max_limits_commands(self, tmp_path): print(f"Extracted commands: {limited_commands}") # Assertions - assert len(limited_commands) == 3, ( - f"Expected 3 commands, got {len(limited_commands)}: {limited_commands}" - ) - assert len(full_commands) >= 3, ( - f"Need at least 3 full commands, got {len(full_commands)}: {full_commands}" - ) - assert limited_commands == full_commands[:3], ( - f"Order not preserved: {limited_commands} vs {full_commands[:3]}" - ) + assert ( + len(limited_commands) == 3 + ), f"Expected 3 commands, got {len(limited_commands)}: {limited_commands}" + assert ( + len(full_commands) >= 3 + ), f"Need at least 3 full commands, got {len(full_commands)}: {full_commands}" + assert ( + limited_commands == full_commands[:3] + ), f"Order not preserved: {limited_commands} vs {full_commands[:3]}" class TestInitForceMtimePreservation: @@ -158,9 +158,9 @@ def test_init_force_no_changes_preserves_mtime(self, tmp_path): # mtime should be preserved mtime_after = devcontainer_file.stat().st_mtime - assert mtime_before == mtime_after, ( - f"mtime changed: {mtime_before} -> {mtime_after}" - ) + assert ( + mtime_before == mtime_after + ), f"mtime changed: {mtime_before} -> {mtime_after}" class TestRepoPathStability: @@ -240,9 +240,9 @@ def test_repo_no_cwd_leakage(self, tmp_path): repo_file = repo_dir / "repro.md" current_file = tmp_path / "repro.md" assert repo_file.exists(), "File not created in repo directory" - assert not current_file.exists(), ( - "File incorrectly created in current directory" - ) + assert ( + not current_file.exists() + ), "File incorrectly created in current directory" class TestOutputFilesEndWithNewline: @@ -364,9 +364,9 @@ def test_keyword_match_shows_command(self, tmp_path): and not line.startswith("#") ): command_lines.append(line) - assert len(command_lines) > 0, ( - f"Expected commands due to pytest keyword match, got: {command_lines}" - ) + assert ( + len(command_lines) > 0 + ), f"Expected commands due to pytest keyword match, got: {command_lines}" # At least one should be pytest related pytest_commands = [line for line in command_lines if "pytest" in line] @@ -404,9 +404,9 @@ def test_language_detection_shows_command(self, tmp_path): and not line.startswith("#") ): command_lines.append(line) - assert len(command_lines) > 0, ( - f"Expected commands due to Python language detection, got: {command_lines}" - ) + assert ( + len(command_lines) > 0 + ), f"Expected commands due to Python language detection, got: {command_lines}" # Should have Python-related commands python_commands = [ @@ -414,9 +414,9 @@ def test_language_detection_shows_command(self, tmp_path): for line in command_lines if any(py in line for py in ["python", "pytest"]) ] - assert len(python_commands) > 0, ( - "Expected Python commands due to language detection" - ) + assert ( + len(python_commands) > 0 + ), "Expected Python commands due to language detection" class TestIntegrationExitCodes: @@ -434,30 +434,30 @@ def test_success_commands_return_zero(self, tmp_path): # Test init --out - success result = run_cli_subprocess(["init", "--out", "-"], cwd=tmp_path) - assert result.returncode == 0, ( - f"init --out - should return 0, got {result.returncode}" - ) + assert ( + result.returncode == 0 + ), f"init --out - should return 0, got {result.returncode}" # Test plan --out - success result = run_cli_subprocess( ["plan", "--desc", "test", "--out", "-"], cwd=tmp_path ) - assert result.returncode == 0, ( - f"plan --out - should return 0, got {result.returncode}" - ) + assert ( + result.returncode == 0 + ), f"plan --out - should return 0, got {result.returncode}" def test_misuse_commands_return_two(self, tmp_path): """Test that CLI misuse returns exit code 2.""" # Test plan without required --desc result = run_cli_subprocess(["plan"], cwd=tmp_path) - assert result.returncode == 2, ( - f"plan without --desc should return 2, got {result.returncode}" - ) + assert ( + result.returncode == 2 + ), f"plan without --desc should return 2, got {result.returncode}" # Test invalid --repo path result = run_cli_subprocess( ["plan", "--desc", "test", "--repo", "/nonexistent/path"], cwd=tmp_path ) - assert result.returncode == 2, ( - f"plan with invalid --repo should return 2, got {result.returncode}" - ) + assert ( + result.returncode == 2 + ), f"plan with invalid --repo should return 2, got {result.returncode}" diff --git a/tests/test_golden_plan.py b/tests/test_golden_plan.py index 15eb668..fa70079 100644 --- a/tests/test_golden_plan.py +++ b/tests/test_golden_plan.py @@ -117,9 +117,9 @@ def test_plan_jest_watch_assertions(): has_relevant = any( word in first_cmd.lower() for word in ["jest", "npm test", "vitest"] ) - assert has_relevant, ( - f"First command should reference jest/npm test/vitest: {first_cmd}" - ) + assert ( + has_relevant + ), f"First command should reference jest/npm test/vitest: {first_cmd}" def test_plan_ambiguous_assertions(): @@ -135,9 +135,9 @@ def test_plan_ambiguous_assertions(): for line in expected_md.split("\n") if " — " in line and not line.startswith("#") ] - assert len(command_lines) <= 3, ( - f"Should have ≤ 3 commands, got {len(command_lines)}" - ) + assert ( + len(command_lines) <= 3 + ), f"Should have ≤ 3 commands, got {len(command_lines)}" # Test JSON structure expected_json_str = read(GOLDEN_DIR / "plan" / "ambiguous.expected.json") diff --git a/tests/test_init.py b/tests/test_init.py index 00f62bf..077c6f8 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -432,9 +432,9 @@ def test_init_force_no_changes_preserves_mtime(self, tmp_path): # mtime should be unchanged since content is identical mtime2 = os.path.getmtime(devcontainer_file) - assert mtime2 == mtime1, ( - f"mtime should be preserved when content is unchanged. mtime1={mtime1}, mtime2={mtime2}" - ) + assert ( + mtime2 == mtime1 + ), f"mtime should be preserved when content is unchanged. mtime1={mtime1}, mtime2={mtime2}" def test_init_dry_run_ignores_force_flag(self, tmp_path): """Test that --dry-run ignores --force flag and outputs to stdout.""" @@ -497,6 +497,6 @@ def test_init_force_no_changes_preserves_mtime_alt(self, tmp_path): # mtime should be unchanged since content is identical mtime2 = os.path.getmtime(devcontainer_file) - assert mtime2 == mtime1, ( - f"mtime should be preserved when content is unchanged. mtime1={mtime1}, mtime2={mtime2}" - ) + assert ( + mtime2 == mtime1 + ), f"mtime should be preserved when content is unchanged. mtime1={mtime1}, mtime2={mtime2}" diff --git a/tests/test_plan_cli.py b/tests/test_plan_cli.py index e328ce0..bf440ae 100644 --- a/tests/test_plan_cli.py +++ b/tests/test_plan_cli.py @@ -135,9 +135,9 @@ def test_plan_requires_input(self, tmp_path, monkeypatch, capsys): # Check that error message mentions one of --desc/--file is required error_output = result.stderr - assert "one of the arguments --desc --file is required" in error_output, ( - f"Expected missing argument error, got: {error_output}" - ) + assert ( + "one of the arguments --desc --file is required" in error_output + ), f"Expected missing argument error, got: {error_output}" def test_plan_writes_md_default_path(self, tmp_path): """Test that plan writes to repro.md by default and contains expected @@ -149,9 +149,9 @@ def test_plan_writes_md_default_path(self, tmp_path): result = run_cli(tmp_path, "--desc", "pytest failing") # Should succeed - assert result.returncode == 0, ( - f"Expected success, got {result.returncode}. STDERR: {result.stderr}" - ) + assert ( + result.returncode == 0 + ), f"Expected success, got {result.returncode}. STDERR: {result.stderr}" # Assert repro.md exists repro_file = tmp_path / "repro.md" @@ -159,9 +159,9 @@ def test_plan_writes_md_default_path(self, tmp_path): # Assert content contains pytest -q in Candidate Commands content = repro_file.read_text() - assert "## Candidate Commands" in content, ( - "Should have Candidate Commands section" - ) + assert ( + "## Candidate Commands" in content + ), "Should have Candidate Commands section" assert "pytest -q" in content, "Should contain pytest -q command" def test_plan_respects_out_and_force(self, tmp_path): @@ -177,9 +177,9 @@ def test_plan_respects_out_and_force(self, tmp_path): result = run_cli(tmp_path, "--desc", "pytest failing", "--out", str(out_path)) # Should succeed and write to docs/ directory - assert result.returncode == 0, ( - f"Expected success, got {result.returncode}. STDERR: {result.stderr}" - ) + assert ( + result.returncode == 0 + ), f"Expected success, got {result.returncode}. STDERR: {result.stderr}" assert docs_dir.exists(), "docs/ directory should exist" assert out_path.exists(), "repro.md should be created in docs/" @@ -194,9 +194,9 @@ def test_plan_respects_out_and_force(self, tmp_path): # Should succeed but not overwrite assert result2.returncode == 0, f"Expected success, got {result2.returncode}" - assert "exists; use --force to overwrite" in result2.stdout, ( - "Should warn about existing file" - ) + assert ( + "exists; use --force to overwrite" in result2.stdout + ), "Should warn about existing file" # mtime should be unchanged mtime_unchanged = os.path.getmtime(out_path) @@ -211,15 +211,15 @@ def test_plan_respects_out_and_force(self, tmp_path): ) # Should succeed and overwrite - assert result3.returncode == 0, ( - f"Expected success, got {result3.returncode}. STDERR: {result3.stderr}" - ) + assert ( + result3.returncode == 0 + ), f"Expected success, got {result3.returncode}. STDERR: {result3.stderr}" # mtime2 should be greater than mtime1 mtime2 = os.path.getmtime(out_path) - assert mtime2 > mtime1, ( - f"File should be modified with --force. mtime1={mtime1}, mtime2={mtime2}" - ) + assert ( + mtime2 > mtime1 + ), f"File should be modified with --force. mtime1={mtime1}, mtime2={mtime2}" def test_plan_infers_env_presence(self, tmp_path): """Test that plan includes Needed Files/Env section and environment @@ -233,9 +233,9 @@ def test_plan_infers_env_presence(self, tmp_path): result = run_cli(tmp_path, "--desc", "anything") # Should succeed - assert result.returncode == 0, ( - f"Expected success, got {result.returncode}. STDERR: {result.stderr}" - ) + assert ( + result.returncode == 0 + ), f"Expected success, got {result.returncode}. STDERR: {result.stderr}" # Assert repro.md contains Needed Files/Env section repro_file = tmp_path / "repro.md" @@ -254,9 +254,9 @@ def test_plan_infers_env_presence(self, tmp_path): elif env_section_found and line.strip().startswith("- "): has_env_content = True break - assert has_env_content, ( - "Should have environment content in Needed Files/Env section" - ) + assert ( + has_env_content + ), "Should have environment content in Needed Files/Env section" def test_plan_node_keywords(self, tmp_path): """Test that plan detects Node keywords and suggests appropriate commands.""" @@ -268,25 +268,25 @@ def test_plan_node_keywords(self, tmp_path): result = run_cli(tmp_path, "--desc", "tests failing on jest") # Should succeed - assert result.returncode == 0, ( - f"Expected success, got {result.returncode}. STDERR: {result.stderr}" - ) + assert ( + result.returncode == 0 + ), f"Expected success, got {result.returncode}. STDERR: {result.stderr}" # Assert output contains either npm test -s or npx jest -w=1 repro_file = tmp_path / "repro.md" assert repro_file.exists(), "repro.md should be created" content = repro_file.read_text() - assert "## Candidate Commands" in content, ( - "Should have Candidate Commands section" - ) + assert ( + "## Candidate Commands" in content + ), "Should have Candidate Commands section" # Should contain either npm test -s or npx jest -w=1 has_npm_test = "npm test -s" in content has_npx_jest = "npx jest -w=1" in content - assert has_npm_test or has_npx_jest, ( - f"Should contain either 'npm test -s' or 'npx jest -w=1' in content: {content}" - ) + assert ( + has_npm_test or has_npx_jest + ), f"Should contain either 'npm test -s' or 'npx jest -w=1' in content: {content}" class TestPlanCLIBasicFunctionality: @@ -648,14 +648,14 @@ def test_max_option_command_ordering_and_counting(self, tmp_path): ] # Verify command count is limited to 3 - assert len(limited_commands) == 3, ( - f"Expected 3 commands, got {len(limited_commands)}: {limited_commands}" - ) + assert ( + len(limited_commands) == 3 + ), f"Expected 3 commands, got {len(limited_commands)}: {limited_commands}" # Verify that limited commands are the first 3 from the full list (proper ordering) - assert len(full_commands) >= 3, ( - f"Need at least 3 commands in full list, got {len(full_commands)}" - ) + assert ( + len(full_commands) >= 3 + ), f"Need at least 3 commands in full list, got {len(full_commands)}" assert limited_commands == full_commands[:3], ( f"Limited should be first 3 of full list.\n" f"Limited: {limited_commands}\nFull[:3]: {full_commands[:3]}" @@ -727,9 +727,9 @@ def test_json_format_output(self, tmp_path, monkeypatch): # Should contain pytest command due to Python detection and keyword commands = [cmd["cmd"] for cmd in data["commands"]] pytest_commands = [cmd for cmd in commands if "pytest" in cmd] - assert len(pytest_commands) > 0, ( - f"Should include pytest commands. Commands: {commands}" - ) + assert ( + len(pytest_commands) > 0 + ), f"Should include pytest commands. Commands: {commands}" def test_json_format_stdout(self, tmp_path): """Test --format json --out - produces JSON to stdout.""" @@ -918,9 +918,9 @@ def test_ambiguous_case_shows_relevant_commands(self, tmp_path): for line in command_lines ] python_commands = [cmd for cmd in commands if "pytest" in cmd] - assert len(python_commands) > 0, ( - f"Should include pytest commands for Python project. Commands: {commands}" - ) + assert ( + len(python_commands) > 0 + ), f"Should include pytest commands for Python project. Commands: {commands}" def test_keyword_match_without_language_detection(self, tmp_path): """Test that specific keywords show relevant commands even without language @@ -950,9 +950,9 @@ def test_keyword_match_without_language_detection(self, tmp_path): ] # Should include npm test or jest commands node_commands = [cmd for cmd in commands if "npm test" in cmd or "jest" in cmd] - assert len(node_commands) > 0, ( - f"Should include npm/jest commands based on keywords. Commands: {commands}" - ) + assert ( + len(node_commands) > 0 + ), f"Should include npm/jest commands based on keywords. Commands: {commands}" def test_no_matches_shows_no_commands(self, tmp_path): """Test that when no keywords or languages match, no commands are shown.""" @@ -973,9 +973,9 @@ def test_no_matches_shows_no_commands(self, tmp_path): ] # Should show NO commands when no keyword or language matches - assert not command_lines, ( - f"Should show no commands when no matches. Got: {command_lines}" - ) + assert ( + not command_lines + ), f"Should show no commands when no matches. Got: {command_lines}" def test_plan_dry_run_ignores_force_flag(self, tmp_path): """Test that --dry-run ignores --force flag and outputs to stdout.""" @@ -1029,9 +1029,9 @@ def test_ambiguous_case_shows_relevant_commands(self, tmp_path): for line in command_lines ] python_commands = [cmd for cmd in commands if "pytest" in cmd] - assert len(python_commands) > 0, ( - f"Should include pytest commands for Python project. Commands: {commands}" - ) + assert ( + len(python_commands) > 0 + ), f"Should include pytest commands for Python project. Commands: {commands}" def test_keyword_match_without_language_detection(self, tmp_path): """Test that specific keywords show relevant commands even without language @@ -1061,9 +1061,9 @@ def test_keyword_match_without_language_detection(self, tmp_path): ] # Should include npm test or jest commands node_commands = [cmd for cmd in commands if "npm test" in cmd or "jest" in cmd] - assert len(node_commands) > 0, ( - f"Should include npm/jest commands based on keywords. Commands: {commands}" - ) + assert ( + len(node_commands) > 0 + ), f"Should include npm/jest commands based on keywords. Commands: {commands}" def test_no_matches_shows_no_commands(self, tmp_path): """Test that when no keywords or languages match, no commands are shown.""" @@ -1084,6 +1084,6 @@ def test_no_matches_shows_no_commands(self, tmp_path): ] # Should show NO commands when no keyword or language matches - assert not command_lines, ( - f"Should show no commands when no matches. Got: {command_lines}" - ) + assert ( + not command_lines + ), f"Should show no commands when no matches. Got: {command_lines}" diff --git a/tests/test_plan_core.py b/tests/test_plan_core.py index 4f25c96..e8b87ef 100644 --- a/tests/test_plan_core.py +++ b/tests/test_plan_core.py @@ -204,9 +204,9 @@ def test_alphabetical_tie_breaking(self): # Check each score group is alphabetically ordered for score, commands in score_groups.items(): - assert commands == sorted(commands), ( - f"Commands with score {score} not alphabetically ordered" - ) + assert commands == sorted( + commands + ), f"Commands with score {score} not alphabetically ordered" def test_detailed_rationales(self): """Test that rationales show matched keywords and detected langs.""" @@ -270,9 +270,9 @@ def test_suggest_commands_weighting(self): vitest_index = next( i for i, (cmd, _, _) in enumerate(suggestions) if cmd == "npx vitest run" ) - assert pytest_index < vitest_index, ( - "pytest -q should appear before npx vitest run in sorted results" - ) + assert ( + pytest_index < vitest_index + ), "pytest -q should appear before npx vitest run in sorted results" class TestSafeTruncate60: @@ -507,19 +507,19 @@ def test_build_repro_md_structure(self): # Assert sections are in correct order expected_order = ["title", "assumptions", "commands", "needs", "next_steps"] actual_order = sorted(section_indices.keys(), key=lambda k: section_indices[k]) - assert actual_order == expected_order, ( - f"Sections not in correct order. Expected {expected_order}, got {actual_order}" - ) + assert ( + actual_order == expected_order + ), f"Sections not in correct order. Expected {expected_order}, got {actual_order}" # Verify section content makes sense assert "Test Issue Title" in lines[section_indices["title"]] - assert any("Test assumption" in line for line in lines), ( - "Assumption content not found" - ) + assert any( + "Test assumption" in line for line in lines + ), "Assumption content not found" assert any("test-cmd" in line for line in lines), "Command content not found" - assert any("Test requirement" in line for line in lines), ( - "Need content not found" - ) + assert any( + "Test requirement" in line for line in lines + ), "Need content not found" assert any("Test step" in line for line in lines), "Next step content not found" diff --git a/tests/test_plan_json_cli.py b/tests/test_plan_json_cli.py index 2dc5b4e..f8af1a2 100644 --- a/tests/test_plan_json_cli.py +++ b/tests/test_plan_json_cli.py @@ -442,9 +442,9 @@ def test_json_stable_command_order(self, tmp_path): # Check scores are in descending order or tied for i in range(1, len(scores)): - assert scores[i] <= scores[i - 1], ( - f"Scores not in descending order: {scores}" - ) + assert ( + scores[i] <= scores[i - 1] + ), f"Scores not in descending order: {scores}" # Within same score, should be alphabetical score_groups = {} @@ -455,6 +455,6 @@ def test_json_stable_command_order(self, tmp_path): score_groups[score].append(cmd["cmd"]) for score, cmds in score_groups.items(): - assert cmds == sorted(cmds), ( - f"Commands with score {score} not alphabetical: {cmds}" - ) + assert cmds == sorted( + cmds + ), f"Commands with score {score} not alphabetical: {cmds}" diff --git a/tests/test_plan_strict_mode.py b/tests/test_plan_strict_mode.py index 3b6adac..661390a 100644 --- a/tests/test_plan_strict_mode.py +++ b/tests/test_plan_strict_mode.py @@ -58,9 +58,9 @@ def test_min_score_filters_low_weight_commands(self): # Test with default min_score=2 - should include Python commands suggestions_default = suggest_commands(keywords, detected_langs, min_score=2) - assert len(suggestions_default) > 0, ( - "Should include Python commands with score >= 2" - ) + assert ( + len(suggestions_default) > 0 + ), "Should include Python commands with score >= 2" # Test with high min_score=4 - should exclude Python-only commands (score 2) suggestions_high = suggest_commands(keywords, detected_langs, min_score=4) @@ -68,9 +68,9 @@ def test_min_score_filters_low_weight_commands(self): # Test with low min_score=1 - should include more commands suggestions_low = suggest_commands(keywords, detected_langs, min_score=1) - assert len(suggestions_low) >= len(suggestions_default), ( - "Lower min_score should include more commands" - ) + assert len(suggestions_low) >= len( + suggestions_default + ), "Lower min_score should include more commands" def test_keyword_match_respects_min_score(self): """Test that direct keyword matches are filtered by min_score.""" @@ -81,9 +81,9 @@ def test_keyword_match_respects_min_score(self): # With min_score=3, should include basic pytest commands suggestions_low = suggest_commands(keywords, detected_langs, min_score=3) - assert len(suggestions_low) > 0, ( - "Should include commands with score >= min_score" - ) + assert ( + len(suggestions_low) > 0 + ), "Should include commands with score >= min_score" # With min_score=5, should exclude all pytest commands (max score is 4) suggestions_high = suggest_commands(keywords, detected_langs, min_score=5) @@ -98,9 +98,9 @@ def test_language_match_respects_min_score(self): # With min_score=2, should include python commands suggestions_low = suggest_commands(keywords, detected_langs, min_score=2) - assert len(suggestions_low) > 0, ( - "Should include commands with score >= min_score" - ) + assert ( + len(suggestions_low) > 0 + ), "Should include commands with score >= min_score" # With min_score=4, should exclude python commands (max score is 3) suggestions_high = suggest_commands(keywords, detected_langs, min_score=4) @@ -129,9 +129,9 @@ def test_strict_mode_exit_0_when_commands_exist(self, tmp_path): ["--desc", "pytest failing", "--min-score", "2", "--strict"], cwd=tmp_path ) - assert result.returncode == 0, ( - f"Expected exit code 0, got {result.returncode}. STDERR: {result.stderr}" - ) + assert ( + result.returncode == 0 + ), f"Expected exit code 0, got {result.returncode}. STDERR: {result.stderr}" def test_non_strict_mode_always_exit_0(self, tmp_path): """Test non-strict mode always exits 0 even with no commands.""" @@ -140,9 +140,9 @@ def test_non_strict_mode_always_exit_0(self, tmp_path): ["--desc", "random generic issue", "--min-score", "5"], cwd=tmp_path ) - assert result.returncode == 0, ( - f"Non-strict mode should always exit 0, got {result.returncode}" - ) + assert ( + result.returncode == 0 + ), f"Non-strict mode should always exit 0, got {result.returncode}" class TestPlanCLIMinScore: @@ -195,9 +195,9 @@ def test_min_score_filters_output(self, tmp_path): ] ) - assert low_commands >= high_commands, ( - "Lower min-score should show same or more commands" - ) + assert ( + low_commands >= high_commands + ), "Lower min-score should show same or more commands" def test_filtering_warning_message(self, tmp_path): """Test that filtering warning message is printed to stderr.""" diff --git a/tests/test_pr_enrichment_integration.py b/tests/test_pr_enrichment_integration.py index 5240f3f..d40be4e 100644 --- a/tests/test_pr_enrichment_integration.py +++ b/tests/test_pr_enrichment_integration.py @@ -188,9 +188,9 @@ def test_pr_comment_create_new(self, fake_env_setup): ) # Verify success - assert result.returncode == 0, ( - f"stdout: {result.stdout}\nstderr: {result.stderr}" - ) + assert ( + result.returncode == 0 + ), f"stdout: {result.stdout}\nstderr: {result.stderr}" # Should create PR comment with sync block assert "Created autorepro comment" in result.stderr @@ -232,9 +232,9 @@ def test_pr_comment_update_existing(self, fake_env_setup): ) # Verify success and that existing comment was updated - assert result.returncode == 0, ( - f"stdout: {result.stdout}\nstderr: {result.stderr}" - ) + assert ( + result.returncode == 0 + ), f"stdout: {result.stdout}\nstderr: {result.stderr}" assert ( "Updated autorepro comment" in result.stderr or "Created autorepro comment" in result.stderr @@ -278,9 +278,9 @@ def test_pr_body_update_sync_block(self, fake_env_setup): ) # Verify success - assert result.returncode == 0, ( - f"stdout: {result.stdout}\nstderr: {result.stderr}" - ) + assert ( + result.returncode == 0 + ), f"stdout: {result.stdout}\nstderr: {result.stderr}" # Should update PR body with sync block assert ( "Updated sync block" in result.stderr @@ -325,9 +325,9 @@ def test_pr_body_update_existing_sync_block(self, fake_env_setup): ) # Verify success and existing sync block was replaced - assert result.returncode == 0, ( - f"stdout: {result.stdout}\nstderr: {result.stderr}" - ) + assert ( + result.returncode == 0 + ), f"stdout: {result.stdout}\nstderr: {result.stderr}" assert ( "Updated sync block" in result.stderr or "Replacing existing sync block" in result.stderr @@ -372,9 +372,9 @@ def test_pr_add_labels(self, fake_env_setup): ) # Verify success - assert result.returncode == 0, ( - f"stdout: {result.stdout}\nstderr: {result.stderr}" - ) + assert ( + result.returncode == 0 + ), f"stdout: {result.stdout}\nstderr: {result.stderr}" # Should add labels to PR assert ( "Added labels" in result.stderr or "Updated PR with labels" in result.stderr @@ -419,9 +419,9 @@ def test_pr_link_issue_cross_reference(self, fake_env_setup): ) # Verify success - assert result.returncode == 0, ( - f"stdout: {result.stdout}\nstderr: {result.stderr}" - ) + assert ( + result.returncode == 0 + ), f"stdout: {result.stdout}\nstderr: {result.stderr}" # Should create cross-reference link assert ( "Cross-linked to issue" in result.stderr @@ -467,9 +467,9 @@ def test_pr_attach_report_metadata(self, fake_env_setup): ) # Verify success - assert result.returncode == 0, ( - f"stdout: {result.stdout}\nstderr: {result.stderr}" - ) + assert ( + result.returncode == 0 + ), f"stdout: {result.stdout}\nstderr: {result.stderr}" # Comment should include report metadata assert ( "Created autorepro comment" in result.stderr @@ -516,9 +516,9 @@ def test_pr_summary_context(self, fake_env_setup): ) # Verify success - assert result.returncode == 0, ( - f"stdout: {result.stdout}\nstderr: {result.stderr}" - ) + assert ( + result.returncode == 0 + ), f"stdout: {result.stdout}\nstderr: {result.stderr}" # Comment should include summary context assert ( "Created autorepro comment" in result.stderr @@ -564,9 +564,9 @@ def test_pr_no_details_flag(self, fake_env_setup): ) # Verify success - assert result.returncode == 0, ( - f"stdout: {result.stdout}\nstderr: {result.stderr}" - ) + assert ( + result.returncode == 0 + ), f"stdout: {result.stdout}\nstderr: {result.stderr}" # Comment should be created without details wrapper assert ( "Created autorepro comment" in result.stderr @@ -620,9 +620,9 @@ def test_pr_all_enrichment_features_combined(self, fake_env_setup): ) # Verify success with all features - assert result.returncode == 0, ( - f"stdout: {result.stdout}\nstderr: {result.stderr}" - ) + assert ( + result.returncode == 0 + ), f"stdout: {result.stdout}\nstderr: {result.stderr}" # Should create/update PR comment, update body, add labels, create cross-links stderr_text = result.stderr assert any( @@ -677,9 +677,9 @@ def test_pr_enrichment_dry_run_mode(self, fake_env_setup): ) # Verify dry-run success - assert result.returncode == 0, ( - f"stdout: {result.stdout}\nstderr: {result.stderr}" - ) + assert ( + result.returncode == 0 + ), f"stdout: {result.stdout}\nstderr: {result.stderr}" # Should show what would be done without actually doing it stdout_text = result.stdout assert "Would run: gh pr create" in stdout_text @@ -729,9 +729,9 @@ def test_pr_enrichment_format_json(self, fake_env_setup): ) # Verify success with JSON format - assert result.returncode == 0, ( - f"stdout: {result.stdout}\nstderr: {result.stderr}" - ) + assert ( + result.returncode == 0 + ), f"stdout: {result.stdout}\nstderr: {result.stderr}" # Comment should be created with JSON format assert ( "Created autorepro comment" in result.stderr @@ -847,7 +847,7 @@ def test_pr_enrichment_mutual_exclusions(self, fake_env_setup): ) # This combination should work (both comment and body updates) - assert result.returncode == 0, ( - f"stdout: {result.stdout}\nstderr: {result.stderr}" - ) + assert ( + result.returncode == 0 + ), f"stdout: {result.stdout}\nstderr: {result.stderr}" assert "Would run: gh pr create" in result.stdout diff --git a/tests/test_repo_stability.py b/tests/test_repo_stability.py index 77c598a..98c6a12 100644 --- a/tests/test_repo_stability.py +++ b/tests/test_repo_stability.py @@ -97,12 +97,12 @@ def test_init_repo_path_resolved_consistently(self, tmp_path): # Both should create devcontainer files in their respective repo directories devcontainer1 = repo_dir1 / ".devcontainer" / "devcontainer.json" devcontainer2 = repo_dir2 / ".devcontainer" / "devcontainer.json" - assert devcontainer1.exists(), ( - "devcontainer.json should be created in repo_dir1" - ) - assert devcontainer2.exists(), ( - "devcontainer.json should be created in repo_dir2" - ) + assert ( + devcontainer1.exists() + ), "devcontainer.json should be created in repo_dir1" + assert ( + devcontainer2.exists() + ), "devcontainer.json should be created in repo_dir2" def test_repo_nonexistent_path_exit_2(self, tmp_path): """Test that nonexistent --repo path returns exit code 2.""" @@ -142,9 +142,9 @@ def test_repo_cwd_not_changed(self, tmp_path): repo_plan_file = repo_dir / "repro.md" current_plan_file = tmp_path / "repro.md" assert repo_plan_file.exists(), "Plan should be created in repo directory" - assert not current_plan_file.exists(), ( - "Plan should NOT be created in current directory" - ) + assert ( + not current_plan_file.exists() + ), "Plan should NOT be created in current directory" # Run init command with --repo result2 = run_cli_subprocess(["init", "--repo", str(repo_dir)], cwd=tmp_path) @@ -156,9 +156,9 @@ def test_repo_cwd_not_changed(self, tmp_path): # Devcontainer should be created in repo directory repo_devcontainer = repo_dir / ".devcontainer" / "devcontainer.json" current_devcontainer = tmp_path / ".devcontainer" / "devcontainer.json" - assert repo_devcontainer.exists(), ( - "Devcontainer should be created in repo directory" - ) - assert not current_devcontainer.exists(), ( - "Devcontainer should NOT be created in current directory" - ) + assert ( + repo_devcontainer.exists() + ), "Devcontainer should be created in repo directory" + assert ( + not current_devcontainer.exists() + ), "Devcontainer should NOT be created in current directory" From ba92d27b1aa89cc5fe4cb0e1c6e39809045f5361 Mon Sep 17 00:00:00 2001 From: Ali Nazzal Date: Mon, 15 Sep 2025 12:32:25 +0300 Subject: [PATCH 8/8] Fix formatTime function formatting to comply with pre-commit ruff-format - Applied ruff-format to formatTime method definitions in logging.py - Changed multi-line function signatures to single line format - All pre-commit hooks now pass successfully - Resolves CI failure in pre-commit workflow Functions affected: - JsonFormatter.formatTime() - KeyValueFormatter.formatTime() --- autorepro/utils/logging.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/autorepro/utils/logging.py b/autorepro/utils/logging.py index 2b51128..9e65753 100644 --- a/autorepro/utils/logging.py +++ b/autorepro/utils/logging.py @@ -63,9 +63,7 @@ def format(self, record: logging.LogRecord) -> str: return json.dumps(payload, separators=(",", ":")) - def formatTime( - self, record: logging.LogRecord, datefmt: str | None = None - ) -> str: # noqa: N802 + def formatTime(self, record: logging.LogRecord, datefmt: str | None = None) -> str: # noqa: N802 ct = self.converter(record.created) if datefmt: s = time.strftime(datefmt, ct) @@ -109,9 +107,7 @@ def format(self, record: logging.LogRecord) -> str: pass return base + (" " + " ".join(extras) if extras else "") - def formatTime( - self, record: logging.LogRecord, datefmt: str | None = None - ) -> str: # noqa: N802 + def formatTime(self, record: logging.LogRecord, datefmt: str | None = None) -> str: # noqa: N802 # ISO8601-ish UTC time with robust msecs handling ct = time.gmtime(record.created) # Safe access to msecs with fallback calculation for compatibility