mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 04:40:45 +08:00
补充历史压缩与会话诊断链路,强化 tool pairing 与空响应兜底观测;完善 Admin MCP 导入/绑定与相关前端展示;新增 web_search_fast/web_fetch_clean 及多项测试与文档更新,并同步技能安装与角色绑定策略改进。 Made-with: Cursor
302 lines
11 KiB
Python
302 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
import zipfile
|
|
import subprocess
|
|
|
|
from oclaw.runtime.skill_installer import (
|
|
auto_install_skill_from_payload,
|
|
create_skill_from_template,
|
|
install_skill_from_local_dir,
|
|
install_skill_from_registry_archive,
|
|
list_skills_with_status,
|
|
repair_skill_dependencies,
|
|
set_skill_enabled,
|
|
)
|
|
from oclaw.platform.persistence.sqlite_store import SqliteStore
|
|
|
|
|
|
def test_create_list_and_disable_skill(tmp_path: Path) -> None:
|
|
db = tmp_path / "ops.sqlite"
|
|
store = SqliteStore(str(db))
|
|
out = create_skill_from_template(
|
|
store=store,
|
|
name="demo_skill",
|
|
description="demo",
|
|
body_markdown="hello",
|
|
skills_root=tmp_path / "skills",
|
|
)
|
|
assert out.ok
|
|
rows = list_skills_with_status(store=store, skills_root=tmp_path / "skills")
|
|
assert len(rows) == 1
|
|
assert rows[0]["name"] == "demo_skill"
|
|
assert rows[0]["enabled"] is True
|
|
set_skill_enabled(store=store, skill_name="demo_skill", enabled=False)
|
|
rows2 = list_skills_with_status(store=store, skills_root=tmp_path / "skills")
|
|
assert rows2[0]["enabled"] is False
|
|
|
|
|
|
def test_auto_install_rollback_on_forced_error(tmp_path: Path) -> None:
|
|
db = tmp_path / "ops.sqlite"
|
|
store = SqliteStore(str(db))
|
|
root = tmp_path / "skills"
|
|
out = auto_install_skill_from_payload(
|
|
store=store,
|
|
payload={
|
|
"name": "roll_me_back",
|
|
"description": "demo",
|
|
"body_markdown": "x",
|
|
"force_error_for_test": True,
|
|
},
|
|
skills_root=root,
|
|
)
|
|
assert out.ok is False
|
|
assert out.retryable is True
|
|
assert out.error_code == "runtime_error"
|
|
assert not (root / "_workspace" / "roll_me_back").exists()
|
|
|
|
|
|
def test_auto_install_private_lane_skips_binding(tmp_path: Path) -> None:
|
|
db = tmp_path / "ops.sqlite"
|
|
store = SqliteStore(str(db))
|
|
root = tmp_path / "skills"
|
|
lane = root / "_workspace" / "generalist"
|
|
out = auto_install_skill_from_payload(
|
|
store=store,
|
|
payload={
|
|
"name": "lane_only_skill",
|
|
"description": "demo",
|
|
"body_markdown": "x",
|
|
},
|
|
skills_root=root,
|
|
workspace_install_parent=lane,
|
|
auto_bind=False,
|
|
)
|
|
assert out.ok is True
|
|
assert out.auto_enabled is False
|
|
assert out.binding_applied_roles == ()
|
|
assert (lane / "lane_only_skill" / "SKILL.md").exists()
|
|
|
|
|
|
def test_auto_install_enables_binding_for_all_roles(tmp_path: Path) -> None:
|
|
db = tmp_path / "ops.sqlite"
|
|
store = SqliteStore(str(db))
|
|
root = tmp_path / "skills"
|
|
out = auto_install_skill_from_payload(
|
|
store=store,
|
|
payload={
|
|
"name": "auto_bound_skill",
|
|
"description": "demo",
|
|
"body_markdown": "x",
|
|
},
|
|
skills_root=root,
|
|
)
|
|
assert out.ok is True
|
|
assert out.auto_enabled is True
|
|
assert len(out.binding_applied_roles) >= 1
|
|
assert (root / "_workspace" / "auto_bound_skill" / "SKILL.md").exists()
|
|
mapping_raw = str(store.get_setting("skill_role_binding") or "").strip()
|
|
assert mapping_raw
|
|
import json
|
|
|
|
mapping = json.loads(mapping_raw)
|
|
assert isinstance(mapping, dict)
|
|
assert any("auto_bound_skill" in (mapping.get(k) or []) for k in mapping.keys())
|
|
|
|
|
|
def test_install_skill_from_registry_archive_file_url(tmp_path: Path) -> None:
|
|
db = tmp_path / "ops.sqlite"
|
|
store = SqliteStore(str(db))
|
|
pkg_dir = tmp_path / "pkg"
|
|
inner = pkg_dir / "demo"
|
|
inner.mkdir(parents=True, exist_ok=True)
|
|
(inner / "SKILL.md").write_text(
|
|
"---\nname: reg_demo\ndescription: x\nmetadata: {\"oclaw\":{}}\n---\n",
|
|
encoding="utf-8",
|
|
)
|
|
archive = tmp_path / "reg.zip"
|
|
with zipfile.ZipFile(archive, "w") as zf:
|
|
zf.write(inner / "SKILL.md", arcname="demo/SKILL.md")
|
|
out = install_skill_from_registry_archive(
|
|
store=store,
|
|
archive_url=archive.resolve().as_uri(),
|
|
skills_root=tmp_path / "skills",
|
|
)
|
|
assert out.ok
|
|
assert out.name == "reg_demo"
|
|
|
|
|
|
def test_install_skill_from_registry_archive_workspace_auto_bind(tmp_path: Path) -> None:
|
|
db = tmp_path / "ops.sqlite"
|
|
store = SqliteStore(str(db))
|
|
pkg_dir = tmp_path / "pkg_ws"
|
|
inner = pkg_dir / "demo"
|
|
inner.mkdir(parents=True, exist_ok=True)
|
|
(inner / "SKILL.md").write_text(
|
|
"---\nname: reg_ws_demo\ndescription: x\nmetadata: {\"oclaw\":{}}\n---\n",
|
|
encoding="utf-8",
|
|
)
|
|
archive = tmp_path / "reg_ws.zip"
|
|
with zipfile.ZipFile(archive, "w") as zf:
|
|
zf.write(inner / "SKILL.md", arcname="demo/SKILL.md")
|
|
root = tmp_path / "skills"
|
|
out = install_skill_from_registry_archive(
|
|
store=store,
|
|
archive_url=archive.resolve().as_uri(),
|
|
skills_root=root / "_workspace",
|
|
auto_bind=True,
|
|
)
|
|
assert out.ok
|
|
assert out.name == "reg_ws_demo"
|
|
assert out.auto_enabled is True
|
|
assert len(out.binding_applied_roles) >= 1
|
|
assert (root / "_workspace" / "reg_ws_demo" / "SKILL.md").exists()
|
|
|
|
|
|
def test_install_skill_from_clawhub_page_url(tmp_path: Path, monkeypatch) -> None:
|
|
db = tmp_path / "ops.sqlite"
|
|
store = SqliteStore(str(db))
|
|
pkg_dir = tmp_path / "pkg_clawhub"
|
|
inner = pkg_dir / "demo"
|
|
inner.mkdir(parents=True, exist_ok=True)
|
|
(inner / "SKILL.md").write_text(
|
|
"---\nname: reg_clawhub_demo\ndescription: x\nmetadata: {\"oclaw\":{}}\n---\n",
|
|
encoding="utf-8",
|
|
)
|
|
archive = tmp_path / "reg_clawhub.zip"
|
|
with zipfile.ZipFile(archive, "w") as zf:
|
|
zf.write(inner / "SKILL.md", arcname="demo/SKILL.md")
|
|
|
|
captured: dict[str, str] = {}
|
|
|
|
def _mock_get_detail(slug: str, *, cfg=None): # noqa: ANN001,ARG001
|
|
captured["slug"] = slug
|
|
return {"archiveUrl": archive.resolve().as_uri()}
|
|
|
|
monkeypatch.setattr("oclaw.runtime.tools.skills.clawhub_client.get_skill_detail", _mock_get_detail)
|
|
|
|
out = install_skill_from_registry_archive(
|
|
store=store,
|
|
archive_url="https://clawhub.ai/pskoett/self-improving-agent",
|
|
skills_root=tmp_path / "skills",
|
|
)
|
|
assert captured["slug"] == "self-improving-agent"
|
|
assert out.ok
|
|
assert out.name == "reg_clawhub_demo"
|
|
|
|
|
|
def test_install_local_allows_sh_files(tmp_path: Path) -> None:
|
|
db = tmp_path / "ops.sqlite"
|
|
store = SqliteStore(str(db))
|
|
src = tmp_path / "local_skill"
|
|
src.mkdir(parents=True, exist_ok=True)
|
|
(src / "SKILL.md").write_text(
|
|
"---\nname: local_with_sh\ndescription: x\nmetadata: {\"oclaw\":{}}\n---\n",
|
|
encoding="utf-8",
|
|
)
|
|
(src / "activator.sh").write_text("#!/usr/bin/env bash\necho ok\n", encoding="utf-8")
|
|
|
|
out = install_skill_from_local_dir(store=store, source_dir=src, skills_root=tmp_path / "skills")
|
|
assert out.ok
|
|
assert out.name == "local_with_sh"
|
|
|
|
|
|
def test_install_local_auto_installs_python_requirements(tmp_path: Path, monkeypatch) -> None:
|
|
db = tmp_path / "ops.sqlite"
|
|
store = SqliteStore(str(db))
|
|
src = tmp_path / "skill_with_reqs"
|
|
src.mkdir(parents=True, exist_ok=True)
|
|
(src / "SKILL.md").write_text(
|
|
"---\nname: with_reqs\ndescription: x\nmetadata: {\"oclaw\":{}}\n---\n",
|
|
encoding="utf-8",
|
|
)
|
|
(src / "requirements.txt").write_text("requests>=2.0.0\n", encoding="utf-8")
|
|
|
|
calls: list[list[str]] = []
|
|
|
|
def _mock_run(cmd, **kwargs): # noqa: ANN001
|
|
calls.append([str(x) for x in cmd])
|
|
return subprocess.CompletedProcess(args=cmd, returncode=0, stdout="", stderr="")
|
|
|
|
monkeypatch.setattr("oclaw.runtime.skill_installer.subprocess.run", _mock_run)
|
|
out = install_skill_from_local_dir(store=store, source_dir=src, skills_root=tmp_path / "skills")
|
|
assert out.ok
|
|
assert any(("pip" in " ".join(c) and "-r" in c) for c in calls)
|
|
|
|
|
|
def test_install_local_dependency_install_failure_returns_warning(tmp_path: Path, monkeypatch) -> None:
|
|
db = tmp_path / "ops.sqlite"
|
|
store = SqliteStore(str(db))
|
|
src = tmp_path / "skill_with_bad_reqs"
|
|
src.mkdir(parents=True, exist_ok=True)
|
|
(src / "SKILL.md").write_text(
|
|
"---\nname: with_bad_reqs\ndescription: x\nmetadata: {\"oclaw\":{}}\n---\n",
|
|
encoding="utf-8",
|
|
)
|
|
(src / "requirements.txt").write_text("not_a_real_pkg_zzz\n", encoding="utf-8")
|
|
|
|
def _mock_run(cmd, **kwargs): # noqa: ANN001,ARG001
|
|
return subprocess.CompletedProcess(args=cmd, returncode=1, stdout="", stderr="install failed")
|
|
|
|
monkeypatch.setattr("oclaw.runtime.skill_installer.subprocess.run", _mock_run)
|
|
out = install_skill_from_local_dir(store=store, source_dir=src, skills_root=tmp_path / "skills")
|
|
assert out.ok
|
|
assert out.detail.startswith("installed_with_dependency_warnings:")
|
|
|
|
|
|
def test_install_local_probe_missing_imports_and_install(tmp_path: Path, monkeypatch) -> None:
|
|
db = tmp_path / "ops.sqlite"
|
|
store = SqliteStore(str(db))
|
|
src = tmp_path / "skill_probe_imports"
|
|
src.mkdir(parents=True, exist_ok=True)
|
|
(src / "SKILL.md").write_text(
|
|
"---\nname: probe_imports\ndescription: x\nmetadata: {\"oclaw\":{}}\n---\n",
|
|
encoding="utf-8",
|
|
)
|
|
(src / "main.py").write_text(
|
|
"import json\nimport office\nimport pandas\nimport totally_missing_pkg_xyz\n",
|
|
encoding="utf-8",
|
|
)
|
|
office_dir = src / "office"
|
|
office_dir.mkdir(parents=True, exist_ok=True)
|
|
(office_dir / "__init__.py").write_text("", encoding="utf-8")
|
|
|
|
calls: list[list[str]] = []
|
|
|
|
def _mock_run(cmd, **kwargs): # noqa: ANN001,ARG001
|
|
calls.append([str(x) for x in cmd])
|
|
return subprocess.CompletedProcess(args=cmd, returncode=0, stdout="", stderr="")
|
|
|
|
monkeypatch.setattr("oclaw.runtime.skill_installer.subprocess.run", _mock_run)
|
|
out = install_skill_from_local_dir(store=store, source_dir=src, skills_root=tmp_path / "skills")
|
|
assert out.ok
|
|
pip_calls = [c for c in calls if ("pip" in " ".join(c))]
|
|
assert pip_calls
|
|
assert any("totally_missing_pkg_xyz" in c for c in pip_calls)
|
|
|
|
|
|
def test_repair_skill_dependencies_for_installed_skill(tmp_path: Path, monkeypatch) -> None:
|
|
db = tmp_path / "ops.sqlite"
|
|
store = SqliteStore(str(db))
|
|
src = tmp_path / "skill_repair"
|
|
src.mkdir(parents=True, exist_ok=True)
|
|
(src / "SKILL.md").write_text(
|
|
"---\nname: skill_repair\ndescription: x\nmetadata: {\"oclaw\":{}}\n---\n",
|
|
encoding="utf-8",
|
|
)
|
|
(src / "main.py").write_text("import definitely_missing_pkg_abc\n", encoding="utf-8")
|
|
|
|
calls: list[list[str]] = []
|
|
|
|
def _mock_run(cmd, **kwargs): # noqa: ANN001,ARG001
|
|
calls.append([str(x) for x in cmd])
|
|
return subprocess.CompletedProcess(args=cmd, returncode=0, stdout="", stderr="")
|
|
|
|
monkeypatch.setattr("oclaw.runtime.skill_installer.subprocess.run", _mock_run)
|
|
out = install_skill_from_local_dir(store=store, source_dir=src, skills_root=tmp_path / "skills")
|
|
assert out.ok
|
|
calls.clear()
|
|
result = repair_skill_dependencies(store=store, skill_name="skill_repair", skills_root=tmp_path / "skills")
|
|
assert bool(result.get("ok")) is True
|
|
assert any("definitely_missing_pkg_abc" in c for c in calls)
|
|
|