539 lines
23 KiB
Python
539 lines
23 KiB
Python
"""Repro: opensquilla bootstrap-template MEMORY.md blocks openclaw migration.
|
|
|
|
When opensquilla initializes a workspace (via ``ensure_agent_workspace``) it
|
|
seeds bootstrap templates for ``SOUL.md`` / ``USER.md`` / ``AGENTS.md`` /
|
|
``MEMORY.md``. These are placeholder docs (5-line comments). Before the fix
|
|
the openclaw migrator's ``_write_text_target`` hit the conflict gate on
|
|
every one of them, silently dropping every workspace file the user actually
|
|
wanted migrated — including the real reason the user invoked the migration,
|
|
their daily memory.
|
|
|
|
The fix detects "destination still holds the pristine bootstrap template"
|
|
and treats it as overwrite-safe (item-level backup + replace), with an
|
|
explicit ``details.replaced_bootstrap_template`` flag in the report.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from opensquilla.identity.bootstrap import ensure_agent_workspace
|
|
from opensquilla.migration.openclaw import MigrationOptions, OpenClawMigrator
|
|
|
|
|
|
def _make_openclaw_source(root: Path) -> Path:
|
|
source = root / ".openclaw"
|
|
workspace = source / "workspace"
|
|
workspace.mkdir(parents=True)
|
|
(workspace / "SOUL.md").write_text("openclaw real soul\n", encoding="utf-8")
|
|
(workspace / "USER.md").write_text("openclaw real user\n", encoding="utf-8")
|
|
(workspace / "AGENTS.md").write_text("openclaw agents guide\n", encoding="utf-8")
|
|
(workspace / "MEMORY.md").write_text("openclaw long-term memory\n", encoding="utf-8")
|
|
(workspace / "memory").mkdir()
|
|
(workspace / "memory" / "2026-05-04.md").write_text(
|
|
"real daily entry that needs to survive migration\n",
|
|
encoding="utf-8",
|
|
)
|
|
(source / "openclaw.json").write_text("{}", encoding="utf-8")
|
|
return source
|
|
|
|
|
|
def test_pristine_bootstrap_templates_do_not_block_migration(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
source = _make_openclaw_source(tmp_path)
|
|
home = tmp_path / "opensquilla-home"
|
|
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(home))
|
|
ensure_agent_workspace(home / "workspace")
|
|
# Confirm the templates were seeded (precondition for the bug).
|
|
for filename in ("SOUL.md", "USER.md", "AGENTS.md", "MEMORY.md"):
|
|
assert (home / "workspace" / filename).is_file()
|
|
|
|
report = OpenClawMigrator(
|
|
MigrationOptions(source=source, config_path=tmp_path / "cfg.toml", apply=True)
|
|
).migrate()
|
|
|
|
statuses = {item["kind"]: item["status"] for item in report["items"]}
|
|
for kind in ("soul", "user-profile", "workspace-agents", "memory"):
|
|
assert statuses.get(kind) == "migrated", (
|
|
f"kind={kind} expected migrated, got {statuses.get(kind)!r}. "
|
|
f"items: {[i for i in report['items'] if i['kind']==kind]}"
|
|
)
|
|
|
|
# Bootstrap-template replacement is announced via the details flag so the
|
|
# report makes the special case visible rather than silent.
|
|
for kind in ("soul", "user-profile", "workspace-agents", "memory"):
|
|
item = next(i for i in report["items"] if i["kind"] == kind)
|
|
assert item["details"].get("replaced_bootstrap_template") is True, (
|
|
f"kind={kind} did not record replaced_bootstrap_template: {item}"
|
|
)
|
|
|
|
# The migrated content actually landed and the daily memory is in MEMORY.md.
|
|
memory_text = (home / "workspace" / "MEMORY.md").read_text(encoding="utf-8")
|
|
assert "real daily entry that needs to survive migration" in memory_text
|
|
|
|
soul_text = (home / "workspace" / "SOUL.md").read_text(encoding="utf-8")
|
|
# rebrand: openclaw -> opensquilla in workspace prose.
|
|
assert "opensquilla real soul" in soul_text
|
|
assert "openclaw" not in soul_text.lower()
|
|
|
|
# Item-level backups of the pristine templates exist for rollback.
|
|
backups = sorted((home / "workspace").glob("*.backup.*"))
|
|
backup_basenames = {b.name.split(".backup.")[0] for b in backups}
|
|
assert {"SOUL.md", "USER.md", "MEMORY.md", "AGENTS.md"} <= backup_basenames
|
|
|
|
|
|
def test_user_edited_memory_is_preserved_and_openclaw_appended(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
# User who has truly edited their MEMORY.md should NOT have their
|
|
# content silently overwritten AND the openclaw memory should NOT be
|
|
# silently dropped. The migrator merges instead: user content stays
|
|
# at the top, openclaw blocks that aren't already there are appended.
|
|
source = _make_openclaw_source(tmp_path)
|
|
home = tmp_path / "opensquilla-home"
|
|
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(home))
|
|
ensure_agent_workspace(home / "workspace")
|
|
(home / "workspace" / "MEMORY.md").write_text(
|
|
"# My real, edited memory\n\nThis is not the template.\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
report = OpenClawMigrator(
|
|
MigrationOptions(source=source, config_path=tmp_path / "cfg.toml", apply=True)
|
|
).migrate()
|
|
|
|
memory_item = next(i for i in report["items"] if i["kind"] == "memory")
|
|
assert memory_item["status"] == "migrated"
|
|
assert memory_item["details"]["appended_to_existing"] is True
|
|
assert memory_item["details"]["new_blocks_appended"] >= 1
|
|
# User content verbatim at the top.
|
|
text = (home / "workspace" / "MEMORY.md").read_text(encoding="utf-8")
|
|
assert text.startswith("# My real, edited memory")
|
|
assert "This is not the template." in text
|
|
# openclaw content appended below.
|
|
assert "real daily entry that needs to survive migration" in text
|
|
# The pre-existing edited file was backed up before being modified.
|
|
backups = list((home / "workspace").glob("MEMORY.md.backup.*"))
|
|
assert len(backups) == 1
|
|
assert (
|
|
"This is not the template."
|
|
in backups[0].read_text(encoding="utf-8")
|
|
)
|
|
|
|
|
|
def test_re_migration_against_existing_destination_dedupes_to_skip(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
# Second pass over an already-migrated destination should record
|
|
# `skipped: all openclaw memory blocks already present in destination`
|
|
# and leave the file untouched. No duplicate growth, no conflict.
|
|
source = _make_openclaw_source(tmp_path)
|
|
home = tmp_path / "opensquilla-home"
|
|
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(home))
|
|
ensure_agent_workspace(home / "workspace")
|
|
|
|
OpenClawMigrator(
|
|
MigrationOptions(source=source, config_path=tmp_path / "cfg.toml", apply=True)
|
|
).migrate()
|
|
after_first = (home / "workspace" / "MEMORY.md").read_text(encoding="utf-8")
|
|
|
|
second = OpenClawMigrator(
|
|
MigrationOptions(source=source, config_path=tmp_path / "cfg.toml", apply=True)
|
|
).migrate()
|
|
memory_item = next(i for i in second["items"] if i["kind"] == "memory")
|
|
assert memory_item["status"] == "skipped"
|
|
assert "already present" in memory_item["reason"]
|
|
assert memory_item["details"]["deduplicated_against_existing"] is True
|
|
after_second = (home / "workspace" / "MEMORY.md").read_text(encoding="utf-8")
|
|
assert after_first == after_second # byte-identical, no second backup either
|
|
|
|
|
|
def test_partial_overlap_appends_only_new_blocks(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
# Destination already contains one of the openclaw daily blocks (e.g. a
|
|
# prior migration's output). Only the genuinely new blocks should be
|
|
# appended; duplicates should be counted in details.
|
|
source = tmp_path / ".openclaw"
|
|
ws = source / "workspace"
|
|
ws.mkdir(parents=True)
|
|
(ws / "memory").mkdir()
|
|
(ws / "memory" / "day-one.md").write_text("shared fact\n", encoding="utf-8")
|
|
(ws / "memory" / "day-two.md").write_text("brand new fact\n", encoding="utf-8")
|
|
(source / "openclaw.json").write_text("{}", encoding="utf-8")
|
|
|
|
home = tmp_path / "opensquilla-home"
|
|
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(home))
|
|
(home / "workspace").mkdir(parents=True)
|
|
# Pre-existing destination already has the day-one block.
|
|
(home / "workspace" / "MEMORY.md").write_text(
|
|
"Unique opensquilla note.\n\n"
|
|
"## Imported daily memory: day-one.md\n\n"
|
|
"shared fact\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
report = OpenClawMigrator(
|
|
MigrationOptions(source=source, config_path=tmp_path / "cfg.toml", apply=True)
|
|
).migrate()
|
|
|
|
memory_item = next(i for i in report["items"] if i["kind"] == "memory")
|
|
assert memory_item["status"] == "migrated"
|
|
assert memory_item["details"]["new_blocks_appended"] == 1
|
|
assert memory_item["details"]["deduplicated_blocks_vs_existing"] >= 1
|
|
|
|
text = (home / "workspace" / "MEMORY.md").read_text(encoding="utf-8")
|
|
assert "Unique opensquilla note" in text
|
|
assert text.count("shared fact") == 1
|
|
assert "brand new fact" in text
|
|
|
|
|
|
def test_persona_conflict_default_in_non_tty_keeps_opensquilla_and_archives(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
# In a non-TTY context (pytest, CI, --json) the default `prompt` mode
|
|
# cannot actually prompt, so it falls back to the safe choice: keep
|
|
# the user's opensquilla content and archive the openclaw original
|
|
# under archive/files/openclaw-orphaned/ so nothing is silently lost.
|
|
source = _make_openclaw_source(tmp_path)
|
|
home = tmp_path / "opensquilla-home"
|
|
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(home))
|
|
(home / "workspace").mkdir(parents=True)
|
|
(home / "workspace" / "SOUL.md").write_text(
|
|
"# My real opensquilla persona\nI am precise and terse.\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
report = OpenClawMigrator(
|
|
MigrationOptions(source=source, config_path=tmp_path / "cfg.toml", apply=True)
|
|
).migrate()
|
|
|
|
soul = next(i for i in report["items"] if i["kind"] == "soul")
|
|
assert soul["status"] == "skipped"
|
|
assert "kept existing opensquilla content" in soul["reason"]
|
|
assert soul["details"]["persona_conflict_resolution"] == "use-opensquilla"
|
|
# Existing opensquilla content untouched.
|
|
assert (
|
|
"I am precise and terse."
|
|
in (home / "workspace" / "SOUL.md").read_text(encoding="utf-8")
|
|
)
|
|
# Openclaw original archived, not silently dropped.
|
|
orphan = Path(report["output_dir"]) / "archive" / "files" / "openclaw-orphaned" / "SOUL.md"
|
|
assert orphan.is_file()
|
|
assert "openclaw real soul" in orphan.read_text(encoding="utf-8")
|
|
|
|
|
|
def test_persona_conflict_use_openclaw_replaces_with_backup(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
source = _make_openclaw_source(tmp_path)
|
|
home = tmp_path / "opensquilla-home"
|
|
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(home))
|
|
(home / "workspace").mkdir(parents=True)
|
|
(home / "workspace" / "SOUL.md").write_text(
|
|
"OPENSQUILLA ORIGINAL\n", encoding="utf-8"
|
|
)
|
|
|
|
OpenClawMigrator(
|
|
MigrationOptions(
|
|
source=source,
|
|
config_path=tmp_path / "cfg.toml",
|
|
apply=True,
|
|
persona_conflict="use-openclaw",
|
|
)
|
|
).migrate()
|
|
|
|
text = (home / "workspace" / "SOUL.md").read_text(encoding="utf-8")
|
|
assert "OPENSQUILLA ORIGINAL" not in text
|
|
# rebrand turns "openclaw real soul" -> "opensquilla real soul"
|
|
assert "opensquilla real soul" in text
|
|
backups = list((home / "workspace").glob("SOUL.md.backup.*"))
|
|
assert len(backups) == 1
|
|
assert "OPENSQUILLA ORIGINAL" in backups[0].read_text(encoding="utf-8")
|
|
|
|
|
|
def test_persona_conflict_merge_appends_with_separator(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
source = _make_openclaw_source(tmp_path)
|
|
home = tmp_path / "opensquilla-home"
|
|
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(home))
|
|
(home / "workspace").mkdir(parents=True)
|
|
(home / "workspace" / "USER.md").write_text(
|
|
"I am Alice, working on RPC layer.\n", encoding="utf-8"
|
|
)
|
|
|
|
report = OpenClawMigrator(
|
|
MigrationOptions(
|
|
source=source,
|
|
config_path=tmp_path / "cfg.toml",
|
|
apply=True,
|
|
persona_conflict="merge",
|
|
)
|
|
).migrate()
|
|
|
|
user_item = next(i for i in report["items"] if i["kind"] == "user-profile")
|
|
assert user_item["status"] == "migrated"
|
|
assert user_item["details"]["persona_conflict_resolution"] == "merge"
|
|
text = (home / "workspace" / "USER.md").read_text(encoding="utf-8")
|
|
# Existing opensquilla content preserved at the top.
|
|
assert text.startswith("I am Alice")
|
|
# OpenClaw content appended after a clear separator.
|
|
assert "## Imported from OpenClaw" in text
|
|
assert "opensquilla real user" in text # rebrand applied
|
|
# Pre-existing file was backed up.
|
|
backups = list((home / "workspace").glob("USER.md.backup.*"))
|
|
assert len(backups) == 1
|
|
|
|
|
|
def test_persona_conflict_skip_drops_both_with_explicit_record(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
source = _make_openclaw_source(tmp_path)
|
|
home = tmp_path / "opensquilla-home"
|
|
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(home))
|
|
(home / "workspace").mkdir(parents=True)
|
|
(home / "workspace" / "AGENTS.md").write_text(
|
|
"WORKSPACE OPERATING RULES\n", encoding="utf-8"
|
|
)
|
|
|
|
report = OpenClawMigrator(
|
|
MigrationOptions(
|
|
source=source,
|
|
config_path=tmp_path / "cfg.toml",
|
|
apply=True,
|
|
persona_conflict="skip",
|
|
)
|
|
).migrate()
|
|
|
|
agents_item = next(i for i in report["items"] if i["kind"] == "workspace-agents")
|
|
assert agents_item["status"] == "skipped"
|
|
assert agents_item["reason"] == "user chose to skip this file"
|
|
assert agents_item["details"]["persona_conflict_resolution"] == "skip"
|
|
# Existing content unchanged.
|
|
assert (
|
|
"WORKSPACE OPERATING RULES"
|
|
in (home / "workspace" / "AGENTS.md").read_text(encoding="utf-8")
|
|
)
|
|
# When user explicitly says "skip" we do NOT archive openclaw — they
|
|
# asked to drop it.
|
|
orphan = Path(report["output_dir"]) / "archive" / "files" / "openclaw-orphaned" / "AGENTS.md"
|
|
assert not orphan.exists()
|
|
|
|
|
|
def test_persona_conflict_use_opensquilla_keeps_dest_and_archives_openclaw(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
# Explicit (non-prompt) use-opensquilla. Same behavior as the non-TTY
|
|
# default but reached via an explicit CLI flag instead of fallback.
|
|
source = _make_openclaw_source(tmp_path)
|
|
home = tmp_path / "opensquilla-home"
|
|
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(home))
|
|
(home / "workspace").mkdir(parents=True)
|
|
(home / "workspace" / "SOUL.md").write_text(
|
|
"EXISTING CONTENT TO PRESERVE\n", encoding="utf-8"
|
|
)
|
|
|
|
report = OpenClawMigrator(
|
|
MigrationOptions(
|
|
source=source,
|
|
config_path=tmp_path / "cfg.toml",
|
|
apply=True,
|
|
persona_conflict="use-opensquilla",
|
|
)
|
|
).migrate()
|
|
|
|
soul = next(i for i in report["items"] if i["kind"] == "soul")
|
|
assert soul["status"] == "skipped"
|
|
assert soul["details"]["persona_conflict_resolution"] == "use-opensquilla"
|
|
assert (
|
|
"EXISTING CONTENT TO PRESERVE"
|
|
in (home / "workspace" / "SOUL.md").read_text(encoding="utf-8")
|
|
)
|
|
orphan = Path(report["output_dir"]) / "archive" / "files" / "openclaw-orphaned" / "SOUL.md"
|
|
assert orphan.is_file()
|
|
|
|
|
|
def test_overwrite_flag_still_replaces_everything(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
# --overwrite is the explicit "replace, do not merge" escape hatch.
|
|
# Confirm its semantics are unchanged by the new merge-by-default flow.
|
|
source = _make_openclaw_source(tmp_path)
|
|
home = tmp_path / "opensquilla-home"
|
|
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(home))
|
|
(home / "workspace").mkdir(parents=True)
|
|
(home / "workspace" / "MEMORY.md").write_text(
|
|
"REAL USER MEMORY THAT GETS REPLACED\n", encoding="utf-8"
|
|
)
|
|
|
|
OpenClawMigrator(
|
|
MigrationOptions(
|
|
source=source, config_path=tmp_path / "cfg.toml", apply=True, overwrite=True
|
|
)
|
|
).migrate()
|
|
|
|
text = (home / "workspace" / "MEMORY.md").read_text(encoding="utf-8")
|
|
assert "REAL USER MEMORY THAT GETS REPLACED" not in text
|
|
assert "real daily entry that needs to survive migration" in text
|
|
# Backup of the previous content is still kept on --overwrite.
|
|
backups = list((home / "workspace").glob("MEMORY.md.backup.*"))
|
|
assert len(backups) == 1
|
|
assert "REAL USER MEMORY THAT GETS REPLACED" in backups[0].read_text(encoding="utf-8")
|
|
|
|
|
|
def test_template_detection_is_robust_to_trailing_whitespace(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
# A stray trailing newline or platform EOL artifact should not disqualify
|
|
# the destination from being treated as the pristine template.
|
|
source = _make_openclaw_source(tmp_path)
|
|
home = tmp_path / "opensquilla-home"
|
|
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(home))
|
|
ensure_agent_workspace(home / "workspace")
|
|
memory_path = home / "workspace" / "MEMORY.md"
|
|
original = memory_path.read_text(encoding="utf-8")
|
|
memory_path.write_text(original.rstrip() + "\n\n\n", encoding="utf-8")
|
|
|
|
report = OpenClawMigrator(
|
|
MigrationOptions(source=source, config_path=tmp_path / "cfg.toml", apply=True)
|
|
).migrate()
|
|
|
|
memory_item = next(i for i in report["items"] if i["kind"] == "memory")
|
|
assert memory_item["status"] == "migrated"
|
|
assert memory_item["details"]["replaced_bootstrap_template"] is True
|
|
|
|
|
|
def test_openclaw_workspace_with_opensquilla_mention_is_kept_verbatim(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
# Same mixed-subject prose bug as hermes, on the openclaw side.
|
|
# Source SOUL.md mentions OpenClaw AND OpenSquilla as distinct
|
|
# entities — mechanical rebrand would corrupt every sentence. The
|
|
# migrator must write the text verbatim and surface
|
|
# ``details.rebrand_skipped``.
|
|
source = _make_openclaw_source(tmp_path)
|
|
(source / "workspace" / "SOUL.md").write_text(
|
|
"OpenClaw v1.2 is installed at ~/.openclaw.\n"
|
|
"OpenSquilla is also installed at ~/.opensquilla and exposes "
|
|
"`migrate openclaw` for importing OpenClaw state.\n",
|
|
encoding="utf-8",
|
|
)
|
|
home = tmp_path / "opensquilla-home"
|
|
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(home))
|
|
|
|
report = OpenClawMigrator(
|
|
MigrationOptions(source=source, config_path=tmp_path / "cfg.toml", apply=True)
|
|
).migrate()
|
|
|
|
migrated = (home / "workspace" / "SOUL.md").read_text(encoding="utf-8")
|
|
# OpenClaw mention preserved verbatim (no rebrand).
|
|
assert "OpenClaw v1.2 is installed at ~/.openclaw" in migrated
|
|
# The "migrate openclaw" command name is kept verbatim too.
|
|
assert "`migrate openclaw`" in migrated
|
|
soul_item = next(i for i in report["items"] if i["kind"] == "soul")
|
|
assert soul_item["details"]["rebrand_skipped"] == "mentions-opensquilla"
|
|
assert "semantic_conversions" not in soul_item["details"]
|
|
|
|
|
|
def test_openclaw_rebrand_does_not_mangle_path_substrings() -> None:
|
|
# Plain string replace turned ``.openclawrc`` into ``.opensquillarc``,
|
|
# ``OpenClawFlavored`` into ``OpenSquillaFlavored``, and
|
|
# ``openclaw_pid`` into ``opensquilla_pid``. None of these were
|
|
# intentional rebrands. Word-boundary aware regex replacement
|
|
# leaves prefix-substring tokens alone.
|
|
from opensquilla.migration.openclaw import _rebrand_text
|
|
|
|
cases = [
|
|
("Config ~/.openclawrc keeps working", "Config ~/.openclawrc keeps working"),
|
|
("var openclaw_pid", "var openclaw_pid"),
|
|
("Has OpenClawFlavored name", "Has OpenClawFlavored name"),
|
|
# Legitimate rebrands:
|
|
("Use ~/.openclaw home", "Use ~/.opensquilla home"),
|
|
("I am OpenClaw", "I am OpenSquilla"),
|
|
("Run: openclaw migrate", "Run: opensquilla migrate"),
|
|
]
|
|
for source_text, expected in cases:
|
|
out, _ = _rebrand_text(source_text)
|
|
assert out == expected, f"{source_text!r} -> {out!r}, expected {expected!r}"
|
|
|
|
|
|
def test_openclaw_mcp_enabled_false_is_preserved(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
# Same fix as hermes side: respect an explicit ``mcp.enabled=false``
|
|
# when the user already has configured servers.
|
|
import json as _json
|
|
|
|
source = tmp_path / ".openclaw"
|
|
(source / "workspace").mkdir(parents=True)
|
|
(source / "openclaw.json").write_text(
|
|
_json.dumps({"mcp": {"servers": {"new": {"command": "/usr/bin/x"}}}}),
|
|
encoding="utf-8",
|
|
)
|
|
home = tmp_path / "opensquilla-home"
|
|
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(home))
|
|
home.mkdir(parents=True, exist_ok=True)
|
|
config_path = tmp_path / "cfg.toml"
|
|
config_path.write_text(
|
|
"[mcp]\nenabled = false\n\n"
|
|
"[[mcp.servers]]\n"
|
|
"name = \"existing\"\ncommand = \"/usr/bin/y\"\ntransport = \"stdio\"\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
report = OpenClawMigrator(
|
|
MigrationOptions(source=source, config_path=config_path, apply=True)
|
|
).migrate()
|
|
|
|
import tomllib
|
|
data = tomllib.loads(config_path.read_text())
|
|
assert data["mcp"]["enabled"] is False
|
|
mc = next(i for i in report["items"] if i["kind"] == "mcp-servers")
|
|
assert mc["details"]["mcp_enabled_left_disabled"] is True
|
|
|
|
|
|
def test_openclaw_memory_blocks_track_skipped_count(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
# Two daily-memory entries: one is pure OpenClaw prose (gets rebranded),
|
|
# the other is mixed-subject (must be kept verbatim). The memory
|
|
# report should record that exactly one block was skipped while the
|
|
# other was rebranded.
|
|
#
|
|
# Note: the openclaw rebrand protects "OpenClaw home/skills/..." as
|
|
# source-reference markers, so we use plain "I am OpenClaw" (which
|
|
# IS rebranded to "I am OpenSquilla") to drive the rebranded branch.
|
|
source = tmp_path / ".openclaw"
|
|
ws = source / "workspace"
|
|
ws.mkdir(parents=True)
|
|
(ws / "memory").mkdir()
|
|
(ws / "memory" / "single.md").write_text(
|
|
"I am OpenClaw and I remember everything.\n",
|
|
encoding="utf-8",
|
|
)
|
|
(ws / "memory" / "mixed.md").write_text(
|
|
"OpenClaw v1.2 lives at ~/.openclaw; OpenSquilla is at ~/.opensquilla.\n",
|
|
encoding="utf-8",
|
|
)
|
|
(source / "openclaw.json").write_text("{}", encoding="utf-8")
|
|
home = tmp_path / "opensquilla-home"
|
|
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(home))
|
|
|
|
report = OpenClawMigrator(
|
|
MigrationOptions(source=source, config_path=tmp_path / "cfg.toml", apply=True)
|
|
).migrate()
|
|
|
|
text = (home / "workspace" / "MEMORY.md").read_text(encoding="utf-8")
|
|
# Rebranded block: "I am OpenClaw" -> "I am OpenSquilla".
|
|
assert "I am OpenSquilla" in text
|
|
# Mixed-subject block: kept verbatim, OpenClaw mention survives.
|
|
assert "OpenClaw v1.2 lives at ~/.openclaw" in text
|
|
memory_item = next(i for i in report["items"] if i["kind"] == "memory")
|
|
assert memory_item["details"]["rebrand_skipped"] == "mentions-opensquilla"
|
|
assert memory_item["details"]["rebrand_skipped_block_count"] == 1
|
|
# The other block was rebranded, so semantic_conversions still appears.
|
|
assert memory_item["details"].get("semantic_conversions") == ["openclaw-branding"]
|