From ad7019d827423aaf10009f99d16b3eb9a6356970 Mon Sep 17 00:00:00 2001 From: Jeremy Brown Date: Wed, 24 Jun 2026 15:17:32 +0200 Subject: [PATCH] fix(correct): graceful message when a batch correction rolls back (refs #1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When apply_corrections rolls a failed batch back (COW atomicity), the correct command now reports a clean "correction failed and was rolled back — no files were changed" instead of surfacing an uncaught OSError traceback. The data was already safe; this just makes the rare failure path user-friendly. --- src/kb/cli.py | 7 +++++++ tests/test_correct.py | 14 ++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/kb/cli.py b/src/kb/cli.py index 4816c5f..a70b50e 100644 --- a/src/kb/cli.py +++ b/src/kb/cli.py @@ -1904,6 +1904,13 @@ def correct( except ValueError as e: click.echo(f"Error: {e}", err=True) raise SystemExit(1) from None + except OSError as e: + # Batch write failed mid-apply; apply_corrections rolled the files back (#1 COW). + click.echo( + f"Error: correction failed and was rolled back — no files were changed ({e}).", + err=True, + ) + raise SystemExit(1) from None finally: kb.close() diff --git a/tests/test_correct.py b/tests/test_correct.py index d160c0d..a4d7dad 100644 --- a/tests/test_correct.py +++ b/tests/test_correct.py @@ -369,6 +369,20 @@ def _invoke(self, runner: CliRunner, memory_tree: Path, args: list[str]): with patch("kb.cli._find_project_root", return_value=project_root): return runner.invoke(cli, ["correct", *args], catch_exceptions=False) + def test_apply_rollback_shows_graceful_error( + self, cli_runner: CliRunner, memory_tree: Path, monkeypatch + ): + """A rolled-back batch (OSError from apply) surfaces a clean message, not a traceback (#1).""" + from kb.api import KnowledgeBase + + def boom(self, *a, **k): # type: ignore[no-untyped-def] + raise OSError("disk full (simulated)") + + monkeypatch.setattr(KnowledgeBase, "correct_term", boom) + result = self._invoke(cli_runner, memory_tree, ["Quartz Indexer", "Datalux", "--apply"]) + assert result.exit_code == 1 + assert "rolled back" in result.output.lower() + def test_scan_json_output(self, cli_runner: CliRunner, memory_tree: Path): """kbx correct TERM --json returns structured scan output.""" result = self._invoke(cli_runner, memory_tree, ["Quartz Indexer", "--json"])