diff --git a/hermes_cli/main.py b/hermes_cli/main.py index a1144314a56c..1db8d66546e4 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -16133,6 +16133,19 @@ def main(): if report.get("backup_path"): print(f" A backup is preserved at: {report['backup_path']}") print(" Keep state.db and the backup; do not delete them.") + # Without this pointer the user is at a dead end: in-place + # repair has failed and nothing tells them the non-destructive + # offline recovery path exists. Lead with --inspect-only so + # they confirm the data is readable before writing anything. + print("") + print(" Next step — offline recovery (never modifies the source):") + source_hint = report.get("backup_path") or db_path + print(f" hermes sessions recover --source {source_hint} \\") + print(" --inspect-only") + print(" If that reports the data is recoverable, rebuild it into") + print(" a NEW database (the active one is left untouched):") + print(f" hermes sessions recover --source {source_hint} \\") + print(" --output recovered-state.db") return if action == "recover": diff --git a/tests/hermes_cli/test_session_recovery.py b/tests/hermes_cli/test_session_recovery.py index eded2b8d5259..39ef1430a0d0 100644 --- a/tests/hermes_cli/test_session_recovery.py +++ b/tests/hermes_cli/test_session_recovery.py @@ -323,3 +323,39 @@ def test_cli_recover_writes_verified_report_without_touching_source( assert report["complete"] is True assert report["verification"]["table_counts"]["sessions"] == expected["sessions"] assert report["verification"]["table_counts"]["messages"] == expected["messages"] + + +def test_failed_repair_points_the_user_at_offline_recovery(tmp_path: Path) -> None: + """A failed in-place repair must name the offline recovery command. + + This is the reported dead end: `hermes sessions repair` exhausts its + in-place ladder, prints "keep the files", and stops -- leaving the user + with no idea that a non-destructive recovery path exists. The failure + branch has to hand them the next command, inspection first, seeded with + the preserved backup it just made. + """ + hermes_home = tmp_path / "isolated-hermes-home" + hermes_home.mkdir() + # Not a database at all -> every in-place repair strategy fails. + (hermes_home / "state.db").write_bytes(b"SQLite format 3\x00" + b"\x7f" * 2048) + env = os.environ.copy() + env["HERMES_HOME"] = str(hermes_home) + + result = subprocess.run( + [sys.executable, "-m", "hermes_cli.main", "sessions", "repair"], + cwd=Path(__file__).resolve().parents[2], + env=env, + capture_output=True, + text=True, + timeout=60, + check=False, + ) + + combined = result.stdout + result.stderr + assert "Repair failed" in combined, combined + # The pointer, and specifically the read-only step first. + assert "sessions recover" in combined, combined + assert "--inspect-only" in combined, combined + # It must offer the source it actually preserved, not a placeholder. + assert "--source" in combined, combined + assert "malformed-backup" in combined, combined