|
|
@@ -79,6 +79,28 @@ class FakeRunner:
|
|
|
return self.results
|
|
|
|
|
|
|
|
|
+class FailingTextStream(StringIO):
|
|
|
+ def __init__(
|
|
|
+ self,
|
|
|
+ *,
|
|
|
+ write_error: BaseException | None = None,
|
|
|
+ flush_error: BaseException | None = None,
|
|
|
+ ) -> None:
|
|
|
+ super().__init__()
|
|
|
+ self.write_error = write_error
|
|
|
+ self.flush_error = flush_error
|
|
|
+
|
|
|
+ def write(self, value: str) -> int:
|
|
|
+ if self.write_error is not None:
|
|
|
+ raise self.write_error
|
|
|
+ return super().write(value)
|
|
|
+
|
|
|
+ def flush(self) -> None:
|
|
|
+ if self.flush_error is not None:
|
|
|
+ raise self.flush_error
|
|
|
+ super().flush()
|
|
|
+
|
|
|
+
|
|
|
def _dependencies(
|
|
|
cli,
|
|
|
*,
|
|
|
@@ -431,6 +453,101 @@ def test_unexpected_runner_failure_still_attempts_report_write(tmp_path: Path):
|
|
|
assert "Bearer [REDACTED]" in stderr.getvalue()
|
|
|
|
|
|
|
|
|
+@pytest.mark.parametrize("failure_point", ["write", "flush"])
|
|
|
+def test_summary_output_failure_after_report_write_exits_two_and_redacts_stderr(
|
|
|
+ tmp_path: Path,
|
|
|
+ failure_point: str,
|
|
|
+):
|
|
|
+ cli = _cli_module()
|
|
|
+ secret = "sk-summary-output-secret"
|
|
|
+ config_path = _write_config(tmp_path / "config.json")
|
|
|
+ output_dir = tmp_path / "reports"
|
|
|
+ error = BrokenPipeError(f"Bearer {secret}")
|
|
|
+ stdout = FailingTextStream(
|
|
|
+ write_error=error if failure_point == "write" else None,
|
|
|
+ flush_error=error if failure_point == "flush" else None,
|
|
|
+ )
|
|
|
+ stderr = StringIO()
|
|
|
+ dependencies = _dependencies(
|
|
|
+ cli,
|
|
|
+ results=[_run_result()],
|
|
|
+ api_key=secret,
|
|
|
+ stdout=stdout,
|
|
|
+ stderr=stderr,
|
|
|
+ )
|
|
|
+
|
|
|
+ exit_code = cli.run_cli(
|
|
|
+ [
|
|
|
+ "--config",
|
|
|
+ str(config_path),
|
|
|
+ "--output-dir",
|
|
|
+ str(output_dir),
|
|
|
+ ],
|
|
|
+ dependencies=dependencies,
|
|
|
+ )
|
|
|
+
|
|
|
+ assert exit_code == 2
|
|
|
+ assert any(output_dir.rglob("*.json"))
|
|
|
+ assert any(output_dir.rglob("*.md"))
|
|
|
+ assert secret not in stderr.getvalue()
|
|
|
+ assert "Bearer [REDACTED]" in stderr.getvalue()
|
|
|
+
|
|
|
+
|
|
|
+def test_execution_error_stderr_failure_still_exits_two(tmp_path: Path):
|
|
|
+ cli = _cli_module()
|
|
|
+ secret = "sk-execution-error-secret"
|
|
|
+ config_path = _write_config(tmp_path / "config.json")
|
|
|
+ output_dir = tmp_path / "reports"
|
|
|
+ stderr = FailingTextStream(
|
|
|
+ write_error=BrokenPipeError(f"Bearer {secret}")
|
|
|
+ )
|
|
|
+ dependencies = _dependencies(
|
|
|
+ cli,
|
|
|
+ results=[],
|
|
|
+ api_key=secret,
|
|
|
+ stderr=stderr,
|
|
|
+ )
|
|
|
+
|
|
|
+ exit_code = cli.run_cli(
|
|
|
+ [
|
|
|
+ "--config",
|
|
|
+ str(config_path),
|
|
|
+ "--output-dir",
|
|
|
+ str(output_dir),
|
|
|
+ ],
|
|
|
+ dependencies=dependencies,
|
|
|
+ )
|
|
|
+
|
|
|
+ assert exit_code == 2
|
|
|
+ assert any(output_dir.rglob("*.json"))
|
|
|
+ assert any(output_dir.rglob("*.md"))
|
|
|
+ assert secret not in stderr.getvalue()
|
|
|
+
|
|
|
+
|
|
|
+def test_summary_output_keyboard_interrupt_still_exits_130(tmp_path: Path):
|
|
|
+ cli = _cli_module()
|
|
|
+ config_path = _write_config(tmp_path / "config.json")
|
|
|
+ output_dir = tmp_path / "reports"
|
|
|
+ dependencies = _dependencies(
|
|
|
+ cli,
|
|
|
+ results=[_run_result()],
|
|
|
+ stdout=FailingTextStream(write_error=KeyboardInterrupt()),
|
|
|
+ )
|
|
|
+
|
|
|
+ exit_code = cli.run_cli(
|
|
|
+ [
|
|
|
+ "--config",
|
|
|
+ str(config_path),
|
|
|
+ "--mock",
|
|
|
+ "--output-dir",
|
|
|
+ str(output_dir),
|
|
|
+ ],
|
|
|
+ dependencies=dependencies,
|
|
|
+ )
|
|
|
+
|
|
|
+ assert exit_code == 130
|
|
|
+
|
|
|
+
|
|
|
@pytest.mark.parametrize(
|
|
|
("results", "attempted", "passed", "failed", "actual_count"),
|
|
|
[
|