fix(sessions): point failed in-place repair at offline recovery

A failed `hermes sessions repair` printed "keep state.db and the backup"
and stopped, so a user whose sessions had vanished had no way to discover
that a non-destructive recovery path exists — the reported dead end.

The failure branch now names the next command, read-only step first, seeded
with the backup path it just preserved. Covered by a CLI-surface regression
test that drives the real subprocess against an unrepairable database.
This commit is contained in:
teknium1 2026-07-25 15:52:06 -07:00 committed by Teknium
parent a9b8128bcb
commit ec2a0f8c1e
2 changed files with 49 additions and 0 deletions

View file

@ -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":

View file

@ -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