mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-21 16:18:55 +00:00
fix(cron): widen UTF-8 BOM tolerance to backup/curator jobs.json readers
Follow-up to the salvaged #66609 (4 primary readers) and #41604 (context files): two more jobs.json readers rejected a BOM'd file — - hermes_cli/backup.py _count_cron_jobs: a BOM made the count None, silently disabling the post-update cron-loss auto-restore safety net - agent/curator_backup.py _backup_cron_jobs_into: BOM broke the job count (spurious parse_warning) and propagated the BOM into snapshots Both now read utf-8-sig; curator snapshots are written BOM-free so rollback restores a file load_jobs can read. AUTHOR_MAP entry added for deacon-botdoctor. Tests: BOM'd-live-file auto-restore + BOM'd snapshot count/BOM-free copy.
This commit is contained in:
parent
f361232883
commit
edfa4cd9b7
4 changed files with 56 additions and 2 deletions
|
|
@ -98,7 +98,12 @@ def _backup_cron_jobs_into(dest: Path) -> Dict[str, Any]:
|
|||
info["reason"] = "no cron/jobs.json present"
|
||||
return info
|
||||
try:
|
||||
raw = src.read_text(encoding="utf-8")
|
||||
# utf-8-sig: same dialect as cron/jobs.load_jobs — a UTF-8 BOM left
|
||||
# by Windows editors otherwise survives decoding as U+FEFF, breaks
|
||||
# json.loads below, and misreports jobs_count as 0 with a spurious
|
||||
# parse warning. The BOM-less text is also what gets written to the
|
||||
# backup, so a later rollback restores a loadable file.
|
||||
raw = src.read_text(encoding="utf-8-sig")
|
||||
except OSError as e:
|
||||
logger.debug("Failed to read cron/jobs.json for backup: %s", e)
|
||||
info["reason"] = f"read error: {e}"
|
||||
|
|
|
|||
|
|
@ -1060,7 +1060,11 @@ def _count_cron_jobs(path: Path) -> Optional[int]:
|
|||
if not path.is_file():
|
||||
return None
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
# utf-8-sig: same dialect as cron/jobs.load_jobs — Windows editors
|
||||
# may leave a UTF-8 BOM that plain utf-8 json.load rejects. Without
|
||||
# it a BOM'd jobs.json counts as "unreadable" (None) and the
|
||||
# post-update cron-loss auto-restore safety net silently disables.
|
||||
with open(path, "r", encoding="utf-8-sig") as f:
|
||||
data = json.load(f)
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -400,6 +400,31 @@ def test_snapshot_cron_jobs_malformed_json_still_captured(backup_env):
|
|||
assert "parse_warning" in mf["cron_jobs"]
|
||||
|
||||
|
||||
def test_snapshot_cron_jobs_utf8_bom_counted_and_backup_bomless(backup_env):
|
||||
"""A UTF-8 BOM on jobs.json (Windows editors) must not break the job
|
||||
count, and the snapshot copy is written BOM-less so rollback restores a
|
||||
file cron/jobs.load_jobs can read."""
|
||||
cb = backup_env["cb"]
|
||||
_write_skill(backup_env["skills"], "alpha")
|
||||
cron_dir = backup_env["home"] / "cron"
|
||||
cron_dir.mkdir()
|
||||
payload = json.dumps({"jobs": [{"id": "job-a"}, {"id": "job-b"}]})
|
||||
(cron_dir / "jobs.json").write_bytes(b"\xef\xbb\xbf" + payload.encode())
|
||||
|
||||
snap = cb.snapshot_skills(reason="test")
|
||||
assert snap is not None
|
||||
|
||||
mf = json.loads((snap / "manifest.json").read_text(encoding="utf-8"))
|
||||
assert mf["cron_jobs"]["backed_up"] is True
|
||||
assert mf["cron_jobs"]["jobs_count"] == 2
|
||||
assert "parse_warning" not in mf["cron_jobs"]
|
||||
|
||||
# Backup copy is decoded text — no BOM survives into the snapshot.
|
||||
backup_bytes = (snap / cb.CRON_JOBS_FILENAME).read_bytes()
|
||||
assert not backup_bytes.startswith(b"\xef\xbb\xbf")
|
||||
assert json.loads(backup_bytes) == json.loads(payload)
|
||||
|
||||
|
||||
def test_rollback_restores_cron_skill_links(backup_env):
|
||||
"""End-to-end: snapshot with job [alpha,beta], curator-style in-place
|
||||
rewrite to [umbrella], then rollback → skills restored to [alpha,beta]."""
|
||||
|
|
|
|||
|
|
@ -2520,6 +2520,26 @@ class TestRestoreCronJobsIfEmptied:
|
|||
result = restore_cron_jobs_if_emptied(snap_id, hermes_home=hermes_home)
|
||||
assert result is None
|
||||
|
||||
def test_bom_live_file_still_counted(self, tmp_path):
|
||||
"""A UTF-8 BOM on the live jobs.json (Windows editors) must not make
|
||||
_count_cron_jobs report None — that would silently disable the
|
||||
auto-restore safety net. utf-8-sig matches cron/jobs.load_jobs."""
|
||||
from hermes_cli.backup import _count_cron_jobs, restore_cron_jobs_if_emptied
|
||||
hermes_home = tmp_path / ".hermes"
|
||||
jobs_path = hermes_home / "cron" / "jobs.json"
|
||||
self._seed_jobs(jobs_path, [{"id": "a"}, {"id": "b"}, {"id": "c"}])
|
||||
snap_id = self._make_snapshot(hermes_home)
|
||||
assert snap_id
|
||||
|
||||
# Migration empties the file AND a Windows editor leaves a BOM.
|
||||
jobs_path.write_bytes(b"\xef\xbb\xbf" + json.dumps({"jobs": []}).encode())
|
||||
assert _count_cron_jobs(jobs_path) == 0 # not None
|
||||
|
||||
result = restore_cron_jobs_if_emptied(snap_id, hermes_home=hermes_home)
|
||||
assert result is not None
|
||||
assert result["restored"] is True
|
||||
assert result["job_count"] == 3
|
||||
|
||||
def test_noop_when_live_file_unreadable(self, tmp_path):
|
||||
"""An unparseable live file is left alone — that's a different failure
|
||||
mode the user should see, not silently overwrite."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue