"""Tests for tools/migrations/import_openclaw_jobs_2026-04.py The script is a one-shot translator. We test: - --dry-run does not mutate the target file - UTC -> Bucharest cron conversion actually changes the hour field - antfarm/* are excluded by the default skip list - clawd path rewrites happen for `cd ~/clawd` and `/home/moltbot/clawd/` - `clawd-archive` / `clawdbot` etc. are NOT matched - Duplicate job names in target cause a skip-with-warning, not a crash """ from __future__ import annotations import importlib.util import json import sys from datetime import datetime, timezone from pathlib import Path import pytest PROJECT_ROOT = Path(__file__).resolve().parents[1] SCRIPT_PATH = ( PROJECT_ROOT / "tools" / "migrations" / "import_openclaw_jobs_2026-04.py" ) @pytest.fixture(scope="module") def mod(): """Load the migration script as a Python module.""" spec = importlib.util.spec_from_file_location( "import_openclaw_jobs_2026_04", SCRIPT_PATH, ) assert spec is not None module = importlib.util.module_from_spec(spec) assert spec.loader is not None spec.loader.exec_module(module) return module # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _make_openclaw_job( name: str, expr: str = "0 6 * * *", tz: str | None = None, enabled: bool = True, message: str = "hello", model: str | None = None, kind: str = "cron", ): sched = {"kind": kind, "expr": expr} if tz: sched["tz"] = tz payload = {"kind": "agentTurn", "message": message} if model: payload["model"] = model return { "id": f"uuid-{name}", "agentId": "echo", "name": name, "enabled": enabled, "schedule": sched, "sessionTarget": "isolated", "payload": payload, "state": {}, "delivery": {"mode": "none"}, } def _write_source(tmp_path: Path, jobs: list[dict]) -> Path: src = tmp_path / "openclaw_jobs.json" src.write_text(json.dumps({"version": 1, "jobs": jobs}), encoding="utf-8") return src def _write_target(tmp_path: Path, jobs: list[dict]) -> Path: tgt = tmp_path / "echo_jobs.json" tgt.write_text(json.dumps(jobs), encoding="utf-8") return tgt # --------------------------------------------------------------------------- # Tests # --------------------------------------------------------------------------- def test_dry_run_does_not_write(mod, tmp_path, capsys): src = _write_source(tmp_path, [_make_openclaw_job("foo")]) tgt = _write_target(tmp_path, []) before = tgt.read_text(encoding="utf-8") rc = mod.run([ "--dry-run", "--source", str(src), "--target", str(tgt), ]) assert rc == 0 after = tgt.read_text(encoding="utf-8") assert before == after captured = capsys.readouterr() assert "DRY-RUN" in captured.out def test_utc_to_bucharest_conversion_shifts_hours(mod): """A UTC cron 0 6 * * * should translate to a non-'0 6' Bucharest expr. Offset varies with DST (+2 or +3), so assert the conversion happened, not the exact value. """ new, warnings = mod.convert_cron_utc_to_bucharest( "0 6 * * *", src_tz=None, ) assert new != "0 6 * * *", f"expected conversion, got {new!r}" # Minute, day-of-month, month, day-of-week must be unchanged. parts = new.split() assert parts[0] == "0" assert parts[2] == "*" assert parts[3] == "*" assert parts[4] == "*" # Hour is now 8 (winter) or 9 (summer). assert parts[1] in {"8", "9"} def test_bucharest_tz_source_is_unchanged(mod): """If openclaw already marks tz=Europe/Bucharest, we must NOT re-shift.""" new, warnings = mod.convert_cron_utc_to_bucharest( "0 7 * * *", src_tz="Europe/Bucharest", ) assert new == "0 7 * * *" assert warnings == [] def test_skip_by_default_excludes_antfarm(mod, tmp_path): src = _write_source(tmp_path, [ _make_openclaw_job("antfarm/feature-dev/planner"), _make_openclaw_job("antfarm/feature-dev/developer"), _make_openclaw_job("keep-this-one"), ]) tgt = _write_target(tmp_path, []) rc = mod.run(["--source", str(src), "--target", str(tgt)]) assert rc == 0 result = json.loads(tgt.read_text(encoding="utf-8")) names = {j["name"] for j in result} assert "keep-this-one" in names assert not any(n.startswith("antfarm/") for n in names) def test_skip_by_default_excludes_night_execute(mod, tmp_path): src = _write_source(tmp_path, [_make_openclaw_job("night-execute")]) tgt = _write_target(tmp_path, []) rc = mod.run(["--source", str(src), "--target", str(tgt)]) assert rc == 0 result = json.loads(tgt.read_text(encoding="utf-8")) assert result == [] def test_youtube_prefix_is_auto_skipped(mod, tmp_path): src = _write_source(tmp_path, [ _make_openclaw_job("YouTube: abc123"), _make_openclaw_job("not-youtube"), ]) tgt = _write_target(tmp_path, []) rc = mod.run(["--source", str(src), "--target", str(tgt)]) assert rc == 0 names = {j["name"] for j in json.loads(tgt.read_text(encoding="utf-8"))} assert "not-youtube" in names assert not any(n.startswith("YouTube:") for n in names) def test_prompt_rewrite_clawd_to_echo_core(mod): text = "Hey, please run this: cd ~/clawd && python3 tools/foo.py" new, subs = mod.rewrite_prompt_paths(text) assert "~/clawd" not in new assert "~/echo-core" in new assert "cd ~/clawd" not in new assert "cd ~/echo-core" in new assert len(subs) == 1 def test_prompt_rewrite_absolute_clawd_path(mod): text = "read /home/moltbot/clawd/memory/foo.md now" new, subs = mod.rewrite_prompt_paths(text) assert "/home/moltbot/echo-core/memory/foo.md" in new assert "/home/moltbot/clawd/memory/foo.md" not in new assert len(subs) == 1 def test_prompt_rewrite_does_not_match_clawd_archive(mod): """Substrings like `clawd-archive` and `clawdbot` must stay untouched.""" text = ( "Folder: /home/moltbot/clawd-archive-old/stuff\n" "Other folder: /home/moltbot/clawdbot/data\n" "cd ~/clawd-archive\n" ) new, subs = mod.rewrite_prompt_paths(text) assert new == text, f"unexpected substitution: {subs}" assert subs == [] def test_prompt_rewrite_cd_clawd_with_trailing_space(mod): # Whitespace boundary on the shell form text = "cd ~/clawd\nls" new, _ = mod.rewrite_prompt_paths(text) assert "cd ~/echo-core" in new def test_duplicate_name_warns_skips(mod, tmp_path, capsys): """When target already has a job 'foo', importing 'foo' must preserve the existing entry and not write a duplicate.""" existing = { "name": "foo", "cron": "0 0 * * *", "channel": "echo-core", "model": "haiku", "prompt": "existing!", "allowed_tools": [], "enabled": True, } tgt = _write_target(tmp_path, [existing]) src = _write_source(tmp_path, [_make_openclaw_job("foo", message="new prompt")]) rc = mod.run(["--source", str(src), "--target", str(tgt)]) assert rc == 0 out = capsys.readouterr().out assert "DUPE" in out assert "already in target" in out result = json.loads(tgt.read_text(encoding="utf-8")) assert len(result) == 1 assert result[0]["prompt"] == "existing!" # untouched assert result[0]["model"] == "haiku" def test_skip_disabled_flag(mod, tmp_path): src = _write_source(tmp_path, [ _make_openclaw_job("enabled-one", enabled=True), _make_openclaw_job("disabled-one", enabled=False), ]) tgt = _write_target(tmp_path, []) rc = mod.run(["--source", str(src), "--target", str(tgt), "--skip-disabled"]) assert rc == 0 names = {j["name"] for j in json.loads(tgt.read_text(encoding="utf-8"))} assert names == {"enabled-one"} def test_non_cron_schedule_is_skipped(mod, tmp_path): # 'at' (one-shot) is not supported in echo-core; must be skipped, not crash. src = _write_source( tmp_path, [_make_openclaw_job("one-shot", kind="at")], ) # Force kind=at by overriding schedule dict data = json.loads(src.read_text(encoding="utf-8")) data["jobs"][0]["schedule"] = { "kind": "at", "at": "2026-02-09T13:00:00.000Z", } src.write_text(json.dumps(data), encoding="utf-8") tgt = _write_target(tmp_path, []) rc = mod.run(["--source", str(src), "--target", str(tgt)]) assert rc == 0 assert json.loads(tgt.read_text(encoding="utf-8")) == [] def test_extra_skip_cli_flag(mod, tmp_path): src = _write_source(tmp_path, [ _make_openclaw_job("a"), _make_openclaw_job("b"), _make_openclaw_job("c"), ]) tgt = _write_target(tmp_path, []) rc = mod.run([ "--source", str(src), "--target", str(tgt), "--skip", "a,c", ]) assert rc == 0 names = {j["name"] for j in json.loads(tgt.read_text(encoding="utf-8"))} assert names == {"b"} def test_default_channel_override(mod, tmp_path): src = _write_source(tmp_path, [_make_openclaw_job("foo")]) tgt = _write_target(tmp_path, []) rc = mod.run([ "--source", str(src), "--target", str(tgt), "--channel", "echo-sprijin", ]) assert rc == 0 data = json.loads(tgt.read_text(encoding="utf-8")) assert data[0]["channel"] == "echo-sprijin" def test_translate_job_preserves_enabled_flag(mod): j = _make_openclaw_job("foo", enabled=True) echo, _ = mod.translate_job(j, "echo-work") assert echo is not None assert echo["enabled"] is True j = _make_openclaw_job("bar", enabled=False) echo, _ = mod.translate_job(j, "echo-work") assert echo is not None assert echo["enabled"] is False def test_translate_job_defaults_model_to_sonnet(mod): j = _make_openclaw_job("foo", model=None) echo, _ = mod.translate_job(j, "echo-work") assert echo is not None assert echo["model"] == "sonnet" def test_translate_job_respects_explicit_model(mod): j = _make_openclaw_job("foo", model="opus") echo, _ = mod.translate_job(j, "echo-work") assert echo is not None assert echo["model"] == "opus"