Files
2026-07-13 13:12:33 +08:00

405 lines
17 KiB
Python

from __future__ import annotations
import json
import tomllib
from pathlib import Path
from typer.testing import CliRunner
from opensquilla.cli.main import app
from opensquilla.gateway.config import (
AgentEntryConfig,
AuthConfig,
GatewayConfig,
RateLimitConfig,
SafetyConfig,
TaskRuntimeConfig,
)
from opensquilla.onboarding.config_store import persist_config
runner = CliRunner()
def _write_realistic_openclaw_home(root: Path) -> Path:
source = root / ".openclaw"
workspace = source / "ops-workspace"
(workspace / "memory").mkdir(parents=True)
(workspace / "skills" / "triage").mkdir(parents=True)
(workspace / ".agents" / "skills" / "workspace-local").mkdir(parents=True)
(workspace / "tts" / "voices").mkdir(parents=True)
(workspace / "hooks").mkdir(parents=True)
(source / "workspace.default").mkdir(parents=True)
(source / "skills" / "research").mkdir(parents=True)
(source / "extensions" / "legacy-plugin").mkdir(parents=True)
(source / "cron").mkdir(parents=True)
(source / "hooks").mkdir(parents=True)
(source / "webhooks").mkdir(parents=True)
(source / "bindings").mkdir(parents=True)
(source / "credentials").mkdir(parents=True)
(source / "devices").mkdir(parents=True)
(source / "identity").mkdir(parents=True)
(source / "memory").mkdir(parents=True)
(source / "agents" / "main" / "agent").mkdir(parents=True)
(workspace / "SOUL.md").write_text("Operate calmly.\n", encoding="utf-8")
(workspace / "AGENTS.md").write_text("Use repo-local tools first.\n", encoding="utf-8")
(workspace / "USER.md").write_text("Prefers concise Chinese reports.\n", encoding="utf-8")
(workspace / "MEMORY.md").write_text("Main memory line.\n", encoding="utf-8")
(workspace / "IDENTITY.md").write_text("Legacy identity overlay.\n", encoding="utf-8")
(workspace / "TOOLS.md").write_text("Legacy tool policy.\n", encoding="utf-8")
(workspace / "HEARTBEAT.md").write_text("Legacy heartbeat.\n", encoding="utf-8")
(workspace / "BOOTSTRAP.md").write_text("Legacy bootstrap.\n", encoding="utf-8")
(workspace / "hooks" / "before_prompt.js").write_text(
"export default function beforePrompt() {}\n",
encoding="utf-8",
)
(workspace / "memory" / "2026-05-11.md").write_text(
"Daily operations note.\n",
encoding="utf-8",
)
(workspace / "skills" / "triage" / "SKILL.md").write_text(
"---\nname: triage\ndescription: Triage production incidents.\n---\n"
"Check impact before mitigation.\n",
encoding="utf-8",
)
(workspace / ".agents" / "skills" / "workspace-local" / "SKILL.md").write_text(
"---\nname: workspace-local\ndescription: Workspace local skill.\n---\n",
encoding="utf-8",
)
(source / "skills" / "research" / "SKILL.md").write_text(
"---\nname: research\ndescription: Source-level research skill.\n---\n",
encoding="utf-8",
)
(workspace / "tts" / "voices" / "ops.txt").write_text(
"voice preset\n",
encoding="utf-8",
)
(source / "workspace.default" / "SOUL.md").write_text(
"Default workspace soul should not win explicit workspace.\n",
encoding="utf-8",
)
(source / "extensions" / "legacy-plugin" / "manifest.json").write_text(
json.dumps({"id": "legacy-plugin", "kind": "extension"}),
encoding="utf-8",
)
(source / "cron" / "store.json").write_text(
json.dumps({"jobs": [{"id": "stored-nightly"}]}),
encoding="utf-8",
)
(source / "hooks" / "gateway-start.js").write_text(
"export default function gatewayStart() {}\n",
encoding="utf-8",
)
(source / "webhooks" / "incoming.json").write_text(
json.dumps({"route": "/legacy"}),
encoding="utf-8",
)
(source / "bindings" / "discord.json").write_text(
json.dumps({"channel": "discord-channel-1"}),
encoding="utf-8",
)
(source / "credentials" / "telegram-default-allowFrom.json").write_text(
json.dumps({"allow": ["123"]}),
encoding="utf-8",
)
(source / "devices" / "device.json").write_text(
json.dumps({"id": "device-1"}),
encoding="utf-8",
)
(source / "identity" / "profile.json").write_text(
json.dumps({"name": "legacy"}),
encoding="utf-8",
)
(source / "memory" / "main.sqlite").write_bytes(b"SQLite format 3\x00")
(source / "workspace.zip").write_bytes(b"not a real zip")
(source / "agents" / "main" / "agent" / "auth-profiles.json").write_text(
json.dumps({"anthropic": {"token": "do-not-copy"}}),
encoding="utf-8",
)
(source / "exec-approvals.json").write_text(
json.dumps({"allow": ["^git status$", "^uv run pytest "]}),
encoding="utf-8",
)
(source / ".env").write_text(
"\n".join(
[
"ANTHROPIC_API_KEY=sk-ant-realistic",
"TELEGRAM_BOT_TOKEN=telegram-secret-token",
"DISCORD_BOT_TOKEN=discord-secret-token",
"SLACK_BOT_TOKEN=xoxb-slack-secret-token",
]
)
+ "\n",
encoding="utf-8",
)
config = {
"agents": {
"defaults": {
"workspace": str(workspace),
"model": "claude-3-5-sonnet-latest",
"timeoutSeconds": 900,
"verboseDefault": True,
"thinkingDefault": "medium",
"compaction": {"mode": "auto", "model": "claude-haiku"},
"humanDelay": {"minMs": 100, "maxMs": 500},
"userTimezone": "Asia/Shanghai",
}
},
"models": {
"providers": {
"anthropic": {
"baseUrl": "https://anthropic.example.test/v1",
}
},
"aliases": {"ops": "claude-3-5-sonnet-latest"},
},
"mcp": {
"servers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "."],
"env": {"NODE_ENV": "test"},
},
"docs": {
"url": "http://127.0.0.1:8765/sse",
"headers": {"X-Test": "openclaw"},
},
}
},
"messages": {
"telegram": {"defaultChatId": "telegram-chat-1"},
"discord": {"defaultChannelId": "discord-channel-1"},
"whatsapp": {"allowedUsers": ["15551234567"]},
"signal": {"account": "+15550000000", "allowedUsers": ["+15551111111"]},
"tts": {"provider": "elevenlabs", "voice": "ops-voice"},
},
"tools": {"exec": {"timeoutSec": 30}, "web": {"enabled": True}},
"plugins": {"enabled": True, "entries": [{"id": "legacy-plugin"}]},
"gateway": {"port": 3000},
"cron": {"jobs": [{"id": "legacy-nightly"}]},
"hooks": {"beforePrompt": ["./hooks/before_prompt.js"]},
"session": {"resetTriggers": ["done"], "sendPolicy": "thread-bound"},
"browser": {"cdpUrl": "http://127.0.0.1:9222", "headless": True},
"approvals": {"mode": "ask"},
"memory": {"backend": "qmd", "vector": {"enabled": True}},
"skills": {"enabled": {"triage": True}, "env": {"triage": {"A": "B"}}},
"ui": {"theme": "dark", "identity": "ops"},
"logging": {"level": "debug"},
"diagnostics": {"otel": True},
}
(source / "openclaw.json").write_text(
json.dumps(
config,
ensure_ascii=False,
),
encoding="utf-8",
)
(source / "clawdbot.json").write_text(
json.dumps({"legacy": "alias-config"}, ensure_ascii=False),
encoding="utf-8",
)
(source / "moltbot.json").write_text(
json.dumps({"legacy": "older-alias-config"}, ensure_ascii=False),
encoding="utf-8",
)
return source
def _write_existing_opensquilla_config(config_path: Path, home: Path) -> None:
cfg = GatewayConfig(
host="127.0.0.9",
port=19999,
auth=AuthConfig(mode="token", token="keep-auth-token"),
rate_limit=RateLimitConfig(max_requests=77, window_seconds=11),
safety=SafetyConfig(injection_scan_mode="enforce"),
task_runtime=TaskRuntimeConfig(max_concurrency=2, max_pending_per_session=9),
search_provider="brave",
agents=[
AgentEntryConfig(
id="ops",
name="Operations",
workspace=str(home / "existing-agent-workspace"),
)
],
)
persist_config(cfg, path=config_path, backup=False)
def test_realistic_cli_apply_is_isolated_and_preserves_unrelated_config(
tmp_path: Path,
monkeypatch,
) -> None:
source = _write_realistic_openclaw_home(tmp_path)
home = tmp_path / "isolated-opensquilla-home"
config_path = tmp_path / "opensquilla.toml"
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(home))
_write_existing_opensquilla_config(config_path, home)
existing_skill = home / "skills" / "openclaw-imports" / "triage" / "SKILL.md"
existing_skill.parent.mkdir(parents=True)
existing_skill.write_text("existing skill stays\n", encoding="utf-8")
unrelated_workspace_file = home / "workspace" / "LOCAL_ONLY.md"
unrelated_workspace_file.parent.mkdir(parents=True)
unrelated_workspace_file.write_text("do not touch\n", encoding="utf-8")
unrelated_state_file = home / "state" / "sessions" / "keep.txt"
unrelated_state_file.parent.mkdir(parents=True)
unrelated_state_file.write_text("session state\n", encoding="utf-8")
result = runner.invoke(
app,
[
"migrate",
"openclaw",
"--source",
str(source),
"--config",
str(config_path),
"--apply",
"--migrate-secrets",
"--skill-conflict",
"rename",
"--json",
],
)
assert result.exit_code == 0, result.stdout
report = json.loads(result.stdout)
encoded_report = json.dumps(report)
assert "sk-ant-realistic" not in encoded_report
assert "telegram-secret-token" not in encoded_report
assert "discord-secret-token" not in encoded_report
assert "xoxb-slack-secret-token" not in encoded_report
config = tomllib.loads(config_path.read_text(encoding="utf-8"))
assert config["auth"]["mode"] == "token"
assert config["auth"]["token"] == "keep-auth-token"
assert config["rate_limit"]["max_requests"] == 77
assert config["rate_limit"]["window_seconds"] == 11
assert config["safety"]["injection_scan_mode"] == "enforce"
assert config["task_runtime"]["max_concurrency"] == 2
assert config["task_runtime"]["max_pending_per_session"] == 9
assert config["search_provider"] == "brave"
assert config["agents"][0]["id"] == "ops"
assert config["llm"]["provider"] == "anthropic"
assert config["llm"]["model"] == "claude-3-5-sonnet-latest"
assert config["llm"]["api_key_env"] == "ANTHROPIC_API_KEY"
assert config["llm"]["base_url"] == "https://anthropic.example.test/v1"
assert config["agent_runtime_timeout_seconds"] == 900
assert config["mcp"]["enabled"] is True
assert {entry["name"] for entry in config["mcp"]["servers"]} == {
"filesystem",
"docs",
}
assert str(home / "skills" / "openclaw-imports") in config["skills"]["extra_dirs"]
channels = {entry["type"]: entry for entry in config["channels"]["channels"]}
assert channels["telegram"]["token"] == "telegram-secret-token"
assert channels["telegram"]["default_chat_id"] == "telegram-chat-1"
assert channels["discord"]["token"] == "discord-secret-token"
assert channels["discord"]["default_channel_id"] == "discord-channel-1"
assert channels["slack"]["token"] == "xoxb-slack-secret-token"
env_text = (home / ".env").read_text(encoding="utf-8")
assert "ANTHROPIC_API_KEY=sk-ant-realistic" in env_text
assert "OPENSQUILLA_SAFE_BIN_ALLOW=^git status$,^uv run pytest " in env_text
assert existing_skill.read_text(encoding="utf-8") == "existing skill stays\n"
renamed_skill = home / "skills" / "openclaw-imports" / "triage-imported-1" / "SKILL.md"
assert "Triage production incidents" in renamed_skill.read_text(encoding="utf-8")
source_skill = home / "skills" / "openclaw-imports" / "research" / "SKILL.md"
assert "Source-level research skill" in source_skill.read_text(encoding="utf-8")
workspace_agent_skill = (
home / "skills" / "openclaw-imports" / "workspace-local" / "SKILL.md"
)
assert "Workspace local skill" in workspace_agent_skill.read_text(encoding="utf-8")
assert unrelated_workspace_file.read_text(encoding="utf-8") == "do not touch\n"
assert unrelated_state_file.read_text(encoding="utf-8") == "session state\n"
workspace = home / "workspace"
assert (workspace / "SOUL.md").read_text(encoding="utf-8") == "Operate calmly.\n"
assert "Daily operations note." in (workspace / "MEMORY.md").read_text(
encoding="utf-8"
)
assert (home / "tts" / "voices" / "ops.txt").read_text(encoding="utf-8") == (
"voice preset\n"
)
output_dir = Path(report["output_dir"])
assert (output_dir / "report.json").is_file()
assert (output_dir / "summary.md").is_file()
assert (output_dir / "archive" / "plugins-config.json").is_file()
assert (output_dir / "archive" / "gateway-config.json").is_file()
assert (output_dir / "archive" / "cron-config.json").is_file()
assert (output_dir / "archive" / "hooks-config.json").is_file()
assert (output_dir / "archive" / "session-config.json").is_file()
assert (output_dir / "archive" / "browser-config.json").is_file()
assert (output_dir / "archive" / "approvals-config.json").is_file()
assert (output_dir / "archive" / "memory-backend-config.json").is_file()
assert (output_dir / "archive" / "skills-registry-config.json").is_file()
assert (output_dir / "archive" / "ui-identity-config.json").is_file()
logging_archive = json.loads(
(output_dir / "archive" / "logging-config.json").read_text(encoding="utf-8")
)
assert logging_archive["logging"]["level"] == "debug"
assert logging_archive["diagnostics"]["otel"] is True
assert (output_dir / "archive" / "tts-config.json").is_file()
assert (output_dir / "archive" / "files" / "workspace" / "IDENTITY.md").is_file()
assert (output_dir / "archive" / "files" / "workspace" / "TOOLS.md").is_file()
assert (output_dir / "archive" / "files" / "workspace" / "HEARTBEAT.md").is_file()
assert (output_dir / "archive" / "files" / "workspace" / "BOOTSTRAP.md").is_file()
assert (
output_dir / "archive" / "files" / "workspace" / "hooks" / "before_prompt.js"
).is_file()
assert (
output_dir / "archive" / "files" / "extensions" / "legacy-plugin" / "manifest.json"
).is_file()
assert (output_dir / "archive" / "files" / "cron" / "store.json").is_file()
assert (output_dir / "archive" / "files" / "hooks" / "gateway-start.js").is_file()
assert (output_dir / "archive" / "files" / "webhooks" / "incoming.json").is_file()
assert (output_dir / "archive" / "files" / "bindings" / "discord.json").is_file()
assert not (output_dir / "archive" / "files" / "credentials").exists()
assert not (output_dir / "archive" / "files" / "devices").exists()
assert not (output_dir / "archive" / "files" / "identity").exists()
assert not (output_dir / "archive" / "files" / "workspace.zip").exists()
assert not (output_dir / "archive" / "files" / "memory" / "main.sqlite").exists()
item_statuses = {(item["kind"], item["status"]) for item in report["items"]}
assert ("credentials", "skipped") in item_statuses
assert ("devices", "skipped") in item_statuses
assert ("identity", "skipped") in item_statuses
assert ("workspace.zip", "skipped") in item_statuses
assert ("memory/main.sqlite", "skipped") in item_statuses
def test_realistic_cli_dry_run_does_not_write_isolated_targets(
tmp_path: Path,
monkeypatch,
) -> None:
source = _write_realistic_openclaw_home(tmp_path)
home = tmp_path / "isolated-opensquilla-home"
config_path = tmp_path / "opensquilla.toml"
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(home))
result = runner.invoke(
app,
[
"migrate",
"openclaw",
"--source",
str(source),
"--config",
str(config_path),
"--migrate-secrets",
"--json",
],
)
assert result.exit_code == 0, result.stdout
report = json.loads(result.stdout)
assert report["apply"] is False
assert any(item["status"] == "planned" for item in report["items"])
assert not config_path.exists()
assert not (home / ".env").exists()
assert not (home / "workspace").exists()
assert not (home / "skills" / "openclaw-imports").exists()