test_openclaw_layout.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. from __future__ import annotations
  2. import json
  3. import os
  4. import stat
  5. from dataclasses import replace
  6. from pathlib import Path
  7. import pytest
  8. from agency_swarm.integrations import openclaw as openclaw_mod
  9. from agency_swarm.integrations.openclaw import OpenClawRuntime
  10. from tests.integration.fastapi._openclaw_test_support import _build_openclaw_config
  11. def test_openclaw_ensure_layout_creates_config_parent_dir(tmp_path: Path) -> None:
  12. config = _build_openclaw_config(tmp_path)
  13. custom_config_path = tmp_path / "external" / "nested" / "openclaw.json"
  14. runtime = OpenClawRuntime(replace(config, config_path=custom_config_path))
  15. runtime.ensure_layout()
  16. assert custom_config_path.exists()
  17. assert custom_config_path.parent.is_dir()
  18. if os.name != "nt":
  19. assert stat.S_IMODE(custom_config_path.stat().st_mode) == 0o600
  20. def test_openclaw_ensure_layout_writes_config_with_secure_create_mode(
  21. tmp_path: Path, monkeypatch: pytest.MonkeyPatch
  22. ) -> None:
  23. config = _build_openclaw_config(tmp_path)
  24. runtime = OpenClawRuntime(config)
  25. original_open = openclaw_mod.os.open
  26. seen_modes: list[int] = []
  27. def _open(path: os.PathLike[str] | str, flags: int, mode: int = 0o777) -> int:
  28. seen_modes.append(mode)
  29. return original_open(path, flags, mode)
  30. monkeypatch.setattr(openclaw_mod.os, "open", _open)
  31. runtime.ensure_layout()
  32. assert seen_modes
  33. assert seen_modes[-1] == 0o600
  34. def test_openclaw_ensure_layout_defaults_workspace_to_home_workspace(tmp_path: Path) -> None:
  35. config = _build_openclaw_config(tmp_path)
  36. runtime = OpenClawRuntime(config)
  37. runtime.ensure_layout()
  38. payload = json.loads(config.config_path.read_text(encoding="utf-8"))
  39. assert payload["agents"]["defaults"]["workspace"] == str(config.workspace_dir)
  40. assert config.workspace_dir.is_dir()
  41. def test_openclaw_ensure_layout_keeps_existing_workspace_override(tmp_path: Path) -> None:
  42. config = _build_openclaw_config(tmp_path)
  43. custom_workspace = tmp_path / "custom-workspace"
  44. config.config_path.parent.mkdir(parents=True, exist_ok=True)
  45. config.config_path.write_text(
  46. json.dumps({"agents": {"defaults": {"workspace": str(custom_workspace)}}}),
  47. encoding="utf-8",
  48. )
  49. OpenClawRuntime(config).ensure_layout()
  50. payload = json.loads(config.config_path.read_text(encoding="utf-8"))
  51. assert payload["agents"]["defaults"]["workspace"] == str(custom_workspace)
  52. assert not config.workspace_dir.exists()
  53. def test_openclaw_ensure_layout_replaces_blank_workspace_override(tmp_path: Path) -> None:
  54. config = _build_openclaw_config(tmp_path)
  55. config.config_path.parent.mkdir(parents=True, exist_ok=True)
  56. config.config_path.write_text(
  57. json.dumps({"agents": {"defaults": {"workspace": " "}}}),
  58. encoding="utf-8",
  59. )
  60. OpenClawRuntime(config).ensure_layout()
  61. payload = json.loads(config.config_path.read_text(encoding="utf-8"))
  62. assert payload["agents"]["defaults"]["workspace"] == str(config.workspace_dir)
  63. assert config.workspace_dir.is_dir()
  64. def test_openclaw_ensure_layout_migrates_legacy_workspace_when_default_is_missing(tmp_path: Path) -> None:
  65. config = _build_openclaw_config(tmp_path)
  66. legacy_workspace = config.legacy_workspace_dir
  67. legacy_workspace.mkdir(parents=True, exist_ok=True)
  68. (legacy_workspace / "AGENTS.md").write_text("legacy", encoding="utf-8")
  69. OpenClawRuntime(config).ensure_layout()
  70. payload = json.loads(config.config_path.read_text(encoding="utf-8"))
  71. assert payload["agents"]["defaults"]["workspace"] == str(config.workspace_dir)
  72. assert (config.workspace_dir / "AGENTS.md").read_text(encoding="utf-8") == "legacy"
  73. assert not legacy_workspace.exists()
  74. def test_openclaw_ensure_layout_preserves_legacy_workspace_when_new_and_old_both_exist(tmp_path: Path) -> None:
  75. config = _build_openclaw_config(tmp_path)
  76. legacy_workspace = config.legacy_workspace_dir
  77. legacy_workspace.mkdir(parents=True, exist_ok=True)
  78. (legacy_workspace / "AGENTS.md").write_text("legacy", encoding="utf-8")
  79. config.workspace_dir.mkdir(parents=True, exist_ok=True)
  80. (config.workspace_dir / "AGENTS.md").write_text("new", encoding="utf-8")
  81. OpenClawRuntime(config).ensure_layout()
  82. payload = json.loads(config.config_path.read_text(encoding="utf-8"))
  83. assert payload["agents"]["defaults"]["workspace"] == str(legacy_workspace)
  84. assert (legacy_workspace / "AGENTS.md").read_text(encoding="utf-8") == "legacy"
  85. assert (config.workspace_dir / "AGENTS.md").read_text(encoding="utf-8") == "new"
  86. def test_openclaw_ensure_layout_merges_legacy_workspace_into_empty_new_dir(tmp_path: Path) -> None:
  87. config = _build_openclaw_config(tmp_path)
  88. legacy_workspace = config.legacy_workspace_dir
  89. legacy_workspace.mkdir(parents=True, exist_ok=True)
  90. (legacy_workspace / "AGENTS.md").write_text("legacy", encoding="utf-8")
  91. config.workspace_dir.mkdir(parents=True, exist_ok=True)
  92. OpenClawRuntime(config).ensure_layout()
  93. payload = json.loads(config.config_path.read_text(encoding="utf-8"))
  94. assert payload["agents"]["defaults"]["workspace"] == str(config.workspace_dir)
  95. assert (config.workspace_dir / "AGENTS.md").read_text(encoding="utf-8") == "legacy"
  96. assert not legacy_workspace.exists()
  97. def test_openclaw_ensure_layout_keeps_explicit_agent_workspace_override(tmp_path: Path) -> None:
  98. config = _build_openclaw_config(tmp_path)
  99. legacy_workspace = config.legacy_workspace_dir
  100. legacy_workspace.mkdir(parents=True, exist_ok=True)
  101. (legacy_workspace / "AGENTS.md").write_text("legacy", encoding="utf-8")
  102. config.config_path.parent.mkdir(parents=True, exist_ok=True)
  103. config.config_path.write_text(
  104. json.dumps({"agents": {"list": [{"id": "main", "workspace": str(legacy_workspace)}]}}),
  105. encoding="utf-8",
  106. )
  107. OpenClawRuntime(config).ensure_layout()
  108. payload = json.loads(config.config_path.read_text(encoding="utf-8"))
  109. assert payload["agents"]["list"][0]["workspace"] == str(legacy_workspace)
  110. assert (legacy_workspace / "AGENTS.md").read_text(encoding="utf-8") == "legacy"
  111. assert not config.workspace_dir.exists()
  112. def test_openclaw_ensure_layout_keeps_default_workspace_migration_when_other_agent_has_override(tmp_path: Path) -> None:
  113. config = _build_openclaw_config(tmp_path)
  114. config.config_path.parent.mkdir(parents=True, exist_ok=True)
  115. specialist_workspace = tmp_path / "specialist-workspace"
  116. config.config_path.write_text(
  117. json.dumps(
  118. {
  119. "agents": {
  120. "list": [
  121. {"id": "main", "default": True},
  122. {"id": "specialist", "workspace": str(specialist_workspace)},
  123. ]
  124. }
  125. }
  126. ),
  127. encoding="utf-8",
  128. )
  129. OpenClawRuntime(config).ensure_layout()
  130. payload = json.loads(config.config_path.read_text(encoding="utf-8"))
  131. assert payload["agents"]["defaults"]["workspace"] == str(config.workspace_dir)
  132. assert payload["agents"]["list"][1]["workspace"] == str(specialist_workspace)
  133. def test_openclaw_ensure_layout_uses_profile_aware_workspace_suffix(tmp_path: Path) -> None:
  134. config = replace(_build_openclaw_config(tmp_path), profile="team-a")
  135. OpenClawRuntime(config).ensure_layout()
  136. payload = json.loads(config.config_path.read_text(encoding="utf-8"))
  137. assert payload["agents"]["defaults"]["workspace"] == str(config.home_dir / "workspace-team-a")
  138. assert (config.home_dir / "workspace-team-a").is_dir()
  139. def test_openclaw_ensure_layout_uses_env_profile_for_manual_config(
  140. tmp_path: Path, monkeypatch: pytest.MonkeyPatch
  141. ) -> None:
  142. monkeypatch.setenv("OPENCLAW_PROFILE", "team-a")
  143. config = _build_openclaw_config(tmp_path)
  144. OpenClawRuntime(config).ensure_layout()
  145. payload = json.loads(config.config_path.read_text(encoding="utf-8"))
  146. assert payload["agents"]["defaults"]["workspace"] == str(config.home_dir / "workspace-team-a")
  147. def test_openclaw_ensure_layout_treats_default_profile_as_unsuffixed_workspace(
  148. tmp_path: Path, monkeypatch: pytest.MonkeyPatch
  149. ) -> None:
  150. monkeypatch.setenv("OPENCLAW_PROFILE", "default")
  151. config = _build_openclaw_config(tmp_path)
  152. OpenClawRuntime(config).ensure_layout()
  153. payload = json.loads(config.config_path.read_text(encoding="utf-8"))
  154. assert payload["agents"]["defaults"]["workspace"] == str(config.home_dir / "workspace")
  155. def test_openclaw_ensure_layout_fails_cleanly_when_workspace_path_is_a_file(tmp_path: Path) -> None:
  156. config = _build_openclaw_config(tmp_path)
  157. config.workspace_dir.parent.mkdir(parents=True, exist_ok=True)
  158. config.workspace_dir.write_text("not-a-dir", encoding="utf-8")
  159. with pytest.raises(RuntimeError, match="workspace path collision"):
  160. OpenClawRuntime(config).ensure_layout()
  161. def test_openclaw_ensure_layout_normalizes_existing_non_dict_config_sections(tmp_path: Path) -> None:
  162. config = _build_openclaw_config(tmp_path)
  163. config.config_path.parent.mkdir(parents=True, exist_ok=True)
  164. config.config_path.write_text(
  165. json.dumps(
  166. {
  167. "gateway": [],
  168. "agents": {"defaults": []},
  169. }
  170. ),
  171. encoding="utf-8",
  172. )
  173. runtime = OpenClawRuntime(config)
  174. runtime.ensure_layout()
  175. saved = json.loads(config.config_path.read_text(encoding="utf-8"))
  176. assert saved["gateway"]["auth"]["mode"] == "token"
  177. assert saved["gateway"]["http"]["endpoints"]["responses"]["enabled"] is True
  178. assert saved["agents"]["defaults"]["model"] == {"primary": "openai/gpt-5.4-mini"}
  179. assert saved["agents"]["defaults"]["workspace"] == str(config.workspace_dir)