完成运行时与启动链路的全面相对路径整改,统一以仓库根为路径基准并移除 oclaw 双层硬编码。

同时补齐路径回归测试与开源自检文档,确保从非仓库 cwd 启动时 workspace 根、配置和 wiki 路径仍稳定可用。

Made-with: Cursor
This commit is contained in:
oliver 2026-04-26 09:22:42 +08:00
parent dbbe3add6a
commit 9d2900db02
40 changed files with 223 additions and 82 deletions

View file

@ -90,3 +90,24 @@ def test_main_hooks_list_invocation(tmp_path, monkeypatch) -> None:
assert code == 0
obj = json.loads(buf.getvalue())
assert "hooks" in obj
def test_resolve_cli_workspace_uses_env_over_cwd(tmp_path, monkeypatch) -> None:
monkeypatch.chdir(tmp_path)
expected = tmp_path / "repo-root"
expected.mkdir(parents=True)
monkeypatch.setenv("OCLAW_WORKSPACE", str(expected))
from oclaw.runtime.operations.hooks_cmd import _resolve_cli_workspace
ns = argparse.Namespace(workspace="")
assert _resolve_cli_workspace(ns) == str(expected)
def test_resolve_cli_workspace_falls_back_to_workspace_root(tmp_path, monkeypatch) -> None:
monkeypatch.chdir(tmp_path)
monkeypatch.delenv("OCLAW_WORKSPACE", raising=False)
monkeypatch.setenv("OPS_WORKSPACE_ROOT", str(tmp_path))
from oclaw.runtime.operations.hooks_cmd import _resolve_cli_workspace
ns = argparse.Namespace(workspace="")
assert _resolve_cli_workspace(ns) == str(tmp_path.resolve())

View file

@ -1,5 +1,6 @@
from __future__ import annotations
from oclaw.platform.config.paths import PROJECT_ROOT
from oclaw.interfaces.http.fastapi_app import _resolve_startup_workspace_dir, _resolve_startup_workspace_dirs
@ -30,3 +31,9 @@ def test_resolve_startup_workspace_dirs_all_agents() -> None:
assert ("main", "D:/project/chatgpt/oclaw/workspace-main") in got_norm
assert ("social", "D:/project/chatgpt/oclaw/workspace-social") in got_norm
def test_resolve_startup_workspace_dir_fallback_to_repo_runtime_main() -> None:
got = _resolve_startup_workspace_dir({})
expect = str((PROJECT_ROOT / "runtime" / "workspaces" / "main").resolve()).replace("\\", "/")
assert got.replace("\\", "/") == expect

View file

@ -6,16 +6,16 @@ from oclaw.platform.config.paths import PROJECT_ROOT
_PROMPT_CRITICAL_FILES = (
"oclaw/runtime/chat/agent.py",
"oclaw/runtime/agents/network_ops_agent.py",
"oclaw/runtime/agents/factory.py",
"oclaw/runtime/chat/agent_messages.py",
"oclaw/runtime/system_prompt.py",
"oclaw/runtime/memory_stage.py",
"oclaw/runtime/project_context_prompt.py",
"oclaw/runtime/chat/agent_errors.py",
"oclaw/platform/llm/image_message_client.py",
"oclaw/runtime/gateway.py",
"runtime/chat/agent.py",
"runtime/agents/network_ops_agent.py",
"runtime/agents/factory.py",
"runtime/chat/agent_messages.py",
"runtime/system_prompt.py",
"runtime/memory_stage.py",
"runtime/project_context_prompt.py",
"runtime/chat/agent_errors.py",
"platform/llm/image_message_client.py",
"runtime/gateway.py",
)
@ -33,8 +33,8 @@ def test_prompt_critical_paths_use_prompt_templates() -> None:
def test_prompt_markdown_frontmatter_keys_present() -> None:
roots = [
PROJECT_ROOT / "oclaw" / "prompts",
PROJECT_ROOT / "oclaw" / "prompts_runtime",
PROJECT_ROOT / "prompts",
PROJECT_ROOT / "prompts_runtime",
]
for prompts_root in roots:
for p in prompts_root.rglob("*.md"):

View file

@ -12,18 +12,18 @@ def _read(rel: str) -> str:
def test_no_legacy_inline_manager_prompts() -> None:
# Manager orchestrator was removed; keep this test as a smoke-guard for any new inline
# manager-style prompt leakage into the runtime entrypoints.
content = _read("oclaw/runtime/gateway.py")
content = _read("runtime/gateway.py")
assert "【重要:只返回合法 JSON】" not in content
assert "你是 AI 助手的路由(Routing)智能体" not in content
def test_no_legacy_inline_specialist_prompts() -> None:
content = _read("oclaw/runtime/agents/specialists.py")
content = _read("runtime/agents/specialists.py")
assert "【最高优先级执行规则(必须遵守)】" not in content
assert "【职责范围】" not in content
def test_no_legacy_inline_runtime_prompt() -> None:
content = _read("oclaw/runtime/chat/agent.py")
content = _read("runtime/chat/agent.py")
assert "你是一个通用 AI 助手。" not in content

View file

@ -12,7 +12,7 @@ from fastapi.testclient import TestClient
from oclaw.interfaces.http.fastapi_app import create_app
from oclaw.platform.config.paths import db_path
from oclaw.platform.persistence.sqlite_store import SqliteStore
from oclaw.runtime.tools.experts.workspace.fs_tools import list_files_tool
from oclaw.runtime.tools.experts.workspace.fs_tools import list_files_tool, write_file_tool
from oclaw.runtime.tools.experts.workspace.workspace_base import (
access_from_env,
build_workspace_path_access,
@ -100,6 +100,21 @@ class WorkspacePathGuardTests(unittest.TestCase):
p = resolve_workspace_path(str(f))
self.assertEqual(p, f.resolve())
def test_write_file_relative_path_defaults_to_data_workspace_subdir(self) -> None:
with mock.patch.dict(
os.environ,
{"OPS_WORKSPACE_ROOT": str(self.root), "OPS_WORKSPACE_EXTRA_ROOTS": "", "OPS_WORKSPACE_ALLOW_ANY_PATH": ""},
clear=False,
):
clear_workspace_path_access_for_tests()
spec = write_file_tool()
with workspace_path_access_scope(None, None):
r = spec.handler({"path": "generated.py", "content": "print('ok')\n", "mode": "overwrite"})
self.assertTrue(r.get("ok"), r)
expected = (self.root / "data" / self.root.name / "generated.py").resolve()
self.assertEqual(str(expected), str(r.get("path")))
self.assertTrue(expected.exists())
def test_per_user_extra_roots_from_db(self) -> None:
f = self.extra / "u.txt"
f.write_text("u", encoding="utf-8")

View file

@ -10,7 +10,7 @@ def _read(path: Path) -> str:
def test_main_dynamic_placeholder_only_in_role_system() -> None:
root = (PROJECT_ROOT / "oclaw" / "runtime" / "workspaces" / "main").resolve()
root = (PROJECT_ROOT / "runtime" / "workspaces" / "main").resolve()
role_system = _read(root / "ROLE_SYSTEM.md")
soul = _read(root / "SOUL.md")
assert "{{MANAGER_DYNAMIC_EXPERTS_HINT}}" in role_system
@ -22,7 +22,7 @@ def test_main_dynamic_placeholder_only_in_role_system() -> None:
def test_main_hard_routing_rule_only_in_role_system() -> None:
root = (PROJECT_ROOT / "oclaw" / "runtime" / "workspaces" / "main").resolve()
root = (PROJECT_ROOT / "runtime" / "workspaces" / "main").resolve()
role_system = _read(root / "ROLE_SYSTEM.md")
assert "回退 `generalist`" in role_system
for name in ("AGENTS.md", "IDENTITY.md", "USER.md", "SOUL.md"):
@ -32,7 +32,7 @@ def test_main_hard_routing_rule_only_in_role_system() -> None:
def test_every_workspace_has_role_system() -> None:
root = (PROJECT_ROOT / "oclaw" / "runtime" / "workspaces").resolve()
root = (PROJECT_ROOT / "runtime" / "workspaces").resolve()
for item in root.iterdir():
if not item.is_dir():
continue
@ -42,7 +42,7 @@ def test_every_workspace_has_role_system() -> None:
def test_workspace_legacy_prompt_files_removed() -> None:
root = (PROJECT_ROOT / "oclaw" / "runtime" / "workspaces").resolve()
root = (PROJECT_ROOT / "runtime" / "workspaces").resolve()
legacy = ("AGENTS.md", "IDENTITY.md", "USER.md")
for item in root.iterdir():
if not item.is_dir():