fix(openclaw-migration): case-preserving brand rewrite + one-time ~/.openclaw residue banner (#16327)

Two related fixes for OpenClaw-residue problems after an OpenClaw→Hermes
migration (especially migrations done via OpenClaw's own tool, which
doesn't archive the source directory).

1. optional-skills/migration/openclaw-migration/scripts/openclaw_to_hermes.py:
   rebrand_text() was rewriting ~/.openclaw/config.yaml → ~/.Hermes/config.yaml
   (capital H — a directory that doesn't exist). Now case-preserving:
   "OpenClaw" → "Hermes" (prose), but "openclaw" → "hermes" (so filesystem
   paths land on the real Hermes home). Regex logic unchanged — replacement
   function now checks if the matched text was all-lowercase and emits the
   replacement in the matching case.

2. agent/onboarding.py + cli.py: one-time startup banner the first time
   Hermes launches and finds ~/.openclaw/. Tells the user to run
   `hermes claw cleanup` to archive it, gated on the existing onboarding
   seen-flag framework (onboarding.seen.openclaw_residue_cleanup in
   config.yaml). Fires once per install; re-running requires wiping that
   flag or running cleanup directly.

Tests:
- 4 new TestDetectOpenclawResidue tests (present / absent / file-instead-
  of-dir / default-home smoke)
- 2 TestOpenclawResidueHint tests (content check)
- 2 TestOpenclawResidueSeenFlag tests (flag isolation + round-trip)
- test_rebrand_text_preserves_filesystem_path_casing regression test
  with 4 scenarios including the exact ~/.openclaw/config.yaml case
- Existing test_rebrand_text_* tests updated to the new case-preserving
  contract (lowercase input → lowercase output)

Co-authored-by: teknium1 <teknium@noreply.github.com>
This commit is contained in:
Teknium
2026-04-26 20:57:26 -07:00
committed by GitHub
parent 517f30b043
commit 6c87371815
5 changed files with 162 additions and 5 deletions

View File

@@ -7,11 +7,14 @@ import pytest
from agent.onboarding import (
BUSY_INPUT_FLAG,
OPENCLAW_RESIDUE_FLAG,
TOOL_PROGRESS_FLAG,
busy_input_hint_cli,
busy_input_hint_gateway,
detect_openclaw_residue,
is_seen,
mark_seen,
openclaw_residue_hint_cli,
tool_progress_hint_cli,
tool_progress_hint_gateway,
)
@@ -176,3 +179,50 @@ class TestRoundTrip:
assert is_seen(loaded, BUSY_INPUT_FLAG) is True
assert is_seen(loaded, TOOL_PROGRESS_FLAG) is True
# ---------------------------------------------------------------------------
# OpenClaw residue banner
# ---------------------------------------------------------------------------
class TestDetectOpenclawResidue:
def test_returns_true_when_openclaw_dir_present(self, tmp_path):
(tmp_path / ".openclaw").mkdir()
assert detect_openclaw_residue(home=tmp_path) is True
def test_returns_false_when_absent(self, tmp_path):
assert detect_openclaw_residue(home=tmp_path) is False
def test_returns_false_when_path_is_a_file(self, tmp_path):
# A stray file named ``.openclaw`` is NOT a workspace — skip the banner.
(tmp_path / ".openclaw").write_text("oops")
assert detect_openclaw_residue(home=tmp_path) is False
def test_default_home_does_not_crash(self):
# Smoke: real $HOME lookup must not raise regardless of state.
assert isinstance(detect_openclaw_residue(), bool)
class TestOpenclawResidueHint:
def test_hint_mentions_cleanup_command(self):
msg = openclaw_residue_hint_cli()
assert "hermes claw cleanup" in msg
assert "~/.openclaw" in msg
def test_hint_not_empty(self):
assert openclaw_residue_hint_cli().strip()
class TestOpenclawResidueSeenFlag:
def test_flag_independent_of_other_flags(self, tmp_path):
cfg_path = tmp_path / "config.yaml"
mark_seen(cfg_path, BUSY_INPUT_FLAG)
loaded = yaml.safe_load(cfg_path.read_text())
assert is_seen(loaded, OPENCLAW_RESIDUE_FLAG) is False
def test_flag_round_trips(self, tmp_path):
cfg_path = tmp_path / "config.yaml"
assert mark_seen(cfg_path, OPENCLAW_RESIDUE_FLAG) is True
loaded = yaml.safe_load(cfg_path.read_text())
assert is_seen(loaded, OPENCLAW_RESIDUE_FLAG) is True