Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cron/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -865,13 +865,13 @@ def load_jobs() -> List[Dict[str, Any]]:
_strict_retry = False # track whether we used the strict=False fallback

try:
with open(jobs_file, 'r', encoding='utf-8') as f:
with open(jobs_file, 'r', encoding='utf-8-sig') as f:
data = json.load(f)
except json.JSONDecodeError:
# Retry with strict=False to handle bare control chars in string values
_strict_retry = True
try:
with open(jobs_file, 'r', encoding='utf-8') as f:
with open(jobs_file, 'r', encoding='utf-8-sig') as f:
data = json.loads(f.read(), strict=False)
except Exception as e:
logger.error("Failed to auto-repair jobs.json: %s", e)
Expand Down
2 changes: 1 addition & 1 deletion hermes_cli/dump.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ def _cron_summary(hermes_home: Path) -> str:
if not jobs_file.exists():
return "0"
try:
with open(jobs_file, encoding="utf-8") as f:
with open(jobs_file, encoding="utf-8-sig") as f:
data = json.load(f)
jobs = data.get("jobs", [])
active = sum(1 for j in jobs if j.get("enabled", True))
Expand Down
2 changes: 1 addition & 1 deletion hermes_cli/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,7 @@ def _resolve_env(env_ref) -> str:
if jobs_file.exists():
import json
try:
with open(jobs_file, encoding="utf-8") as f:
with open(jobs_file, encoding="utf-8-sig") as f:
data = json.load(f)
jobs = data.get("jobs", [])
enabled_jobs = [j for j in jobs if j.get("enabled", True)]
Expand Down
7 changes: 7 additions & 0 deletions tests/cron/test_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,13 @@ def test_list_jobs(self, tmp_cron_dir):
jobs = list_jobs()
assert len(jobs) == 2

def test_load_jobs_reads_utf8_bom_store(self, tmp_cron_dir):
jobs_file = tmp_cron_dir / "cron" / "jobs.json"
jobs_file.parent.mkdir(parents=True)
jobs_file.write_bytes(b'\xef\xbb\xbf{"jobs": [{"id": "bom-job", "enabled": true}]}')

assert load_jobs() == [{"id": "bom-job", "enabled": True}]

def test_list_jobs_normalizes_partial_legacy_records(self, tmp_cron_dir):
save_jobs([
{
Expand Down
10 changes: 10 additions & 0 deletions tests/hermes_cli/test_dump_env_visibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,13 @@ def test_dump_leaves_unset_key_untouched(monkeypatch, capsys, tmp_path):
line = _api_key_line(capsys.readouterr().out, "tavily")
assert "not set" in line
assert "shell only" not in line


def test_cron_summary_reads_utf8_bom_jobs_file(tmp_path):
from hermes_cli.dump import _cron_summary

jobs_file = tmp_path / "cron" / "jobs.json"
jobs_file.parent.mkdir(parents=True)
jobs_file.write_bytes(b'\xef\xbb\xbf{"jobs": [{"id": "bom-job", "enabled": true}]}')

assert _cron_summary(tmp_path) == "1 active / 1 total"
19 changes: 19 additions & 0 deletions tests/hermes_cli/test_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,25 @@ def test_show_status_reports_nous_inference_key_without_portal_login(monkeypatch
# Helpers shared by xAI OAuth status tests
# ---------------------------------------------------------------------------

def test_show_status_reads_utf8_bom_jobs_file(monkeypatch, capsys, tmp_path):
from hermes_cli import status as status_mod

jobs_file = tmp_path / "cron" / "jobs.json"
jobs_file.parent.mkdir(parents=True)
jobs_file.write_bytes(b'\xef\xbb\xbf{"jobs": [{"id": "bom-job", "enabled": true}]}')

monkeypatch.setattr(status_mod, "get_env_path", lambda: tmp_path / ".env", raising=False)
monkeypatch.setattr(status_mod, "get_hermes_home", lambda: tmp_path, raising=False)
monkeypatch.setattr(status_mod, "load_config", lambda: {"model": "gpt-5.4"}, raising=False)
monkeypatch.setattr(status_mod, "resolve_requested_provider", lambda requested=None: "openai-codex", raising=False)
monkeypatch.setattr(status_mod, "resolve_provider", lambda requested=None, **kwargs: "openai-codex", raising=False)
monkeypatch.setattr(status_mod, "provider_label", lambda provider: "OpenAI Codex", raising=False)

status_mod.show_status(SimpleNamespace(all=False, deep=False))

assert "Jobs: 1 active, 1 total" in capsys.readouterr().out


def _base_xai_mocks(monkeypatch, tmp_path):
"""Set up the minimal environment for show_status, returning status_mod."""
from hermes_cli import status as status_mod
Expand Down