mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 03:23:23 +08:00
- Rename platform/ to svc/ to avoid shadowing stdlib platform. - Replace from oclaw.* with from svc/runtime/interfaces; update -m CLI paths. - tests/conftest: prepend repo root to sys.path (no parent-folder package name). - CI: paths and offline_eval script under repo root. - Ops scripts: PYTHONPATH must be repo root for python -m runtime.* (fixes gateway/WhatsApp sidecar startup). - Fix default oclaw.json path in tabular/file attachment limits; stabilize attachment test config. Co-authored-by: Cursor <cursoragent@cursor.com>
324 lines
9.6 KiB
Python
324 lines
9.6 KiB
Python
from __future__ import annotations
|
||
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
from runtime.skills import workspace_skills_layout_signature
|
||
from runtime.system_prompt import build_oclaw_executor_system_prompt
|
||
from runtime.types import OclawMemoryContext
|
||
from svc.persistence.sqlite_store import SqliteStore
|
||
from runtime.tools.base import ToolRegistry, ToolSpec
|
||
|
||
|
||
def _spec(name: str) -> ToolSpec:
|
||
return ToolSpec(
|
||
name=name,
|
||
description="d",
|
||
parameters={"type": "object", "properties": {}},
|
||
handler=lambda _a: {"ok": True},
|
||
read_only=True,
|
||
)
|
||
|
||
|
||
def test_build_oclaw_executor_system_prompt_includes_skills_block(tmp_path: Path) -> None:
|
||
store = SqliteStore(str(tmp_path / "sp.sqlite"))
|
||
store.create_session("s")
|
||
reg = ToolRegistry([_spec("demo_tool")])
|
||
out = build_oclaw_executor_system_prompt(
|
||
store=store,
|
||
tools=reg,
|
||
base_url="",
|
||
base_system="You are helpful.",
|
||
memory_context=OclawMemoryContext(),
|
||
lang="zh",
|
||
)
|
||
assert "## 技能(skills):" in out
|
||
assert '- name:"' in out
|
||
|
||
|
||
def test_build_oclaw_executor_system_prompt_without_tools_has_no_skills_block(tmp_path: Path) -> None:
|
||
store = SqliteStore(str(tmp_path / "sp1.sqlite"))
|
||
store.create_session("s")
|
||
out = build_oclaw_executor_system_prompt(
|
||
store=store,
|
||
tools=None,
|
||
base_url="",
|
||
base_system=" plain ",
|
||
memory_context=OclawMemoryContext(),
|
||
lang="zh",
|
||
)
|
||
assert "## 技能(skills):" not in out
|
||
assert "不会自动执行技能 `scripts/` 目录下的文件" in out
|
||
assert "scripts/" in out
|
||
|
||
|
||
def test_build_oclaw_executor_system_prompt_with_tools_has_skills(tmp_path: Path) -> None:
|
||
store = SqliteStore(str(tmp_path / "sp2.sqlite"))
|
||
store.create_session("s")
|
||
reg = ToolRegistry([_spec("x")])
|
||
out = build_oclaw_executor_system_prompt(
|
||
store=store,
|
||
tools=reg,
|
||
base_url="",
|
||
base_system="base",
|
||
memory_context=OclawMemoryContext(),
|
||
lang="zh",
|
||
)
|
||
assert "## 技能(skills):" in out
|
||
assert '- name:"' in out
|
||
|
||
|
||
def test_executor_static_prompt_cache_invalidates_on_settings_change(monkeypatch: pytest.MonkeyPatch) -> None:
|
||
from runtime import system_prompt as sp
|
||
|
||
class DummyStore:
|
||
def __init__(self) -> None:
|
||
self.values: dict[str, str] = {}
|
||
|
||
def get_setting(self, key: str) -> str:
|
||
return str(self.values.get(key, ""))
|
||
|
||
class DummyReg:
|
||
pass
|
||
|
||
calls = {"n": 0}
|
||
|
||
def _skills_block(**kwargs) -> str:
|
||
_ = kwargs
|
||
calls["n"] += 1
|
||
return "skills-block"
|
||
|
||
monkeypatch.setattr(sp, "expert_workspace_signature_token", lambda: ("sig",))
|
||
monkeypatch.setattr(sp, "build_project_context_block", lambda **kwargs: "")
|
||
monkeypatch.setattr(sp, "build_skills_catalog_block", _skills_block)
|
||
monkeypatch.setattr(
|
||
sp,
|
||
"render_runtime_prompt",
|
||
lambda prompt_id, variables, strict: f"{prompt_id}\n{variables.get('skills_catalog') or ''}",
|
||
)
|
||
|
||
store = DummyStore()
|
||
reg = DummyReg()
|
||
_ = sp.get_executor_prompt_static(
|
||
store=store,
|
||
tools=reg, # type: ignore[arg-type]
|
||
base_url="",
|
||
base_system="base",
|
||
workspace_dir=None,
|
||
skill_binding_role="generalist",
|
||
)
|
||
_ = sp.get_executor_prompt_static(
|
||
store=store,
|
||
tools=reg, # type: ignore[arg-type]
|
||
base_url="",
|
||
base_system="base",
|
||
workspace_dir=None,
|
||
skill_binding_role="generalist",
|
||
)
|
||
assert calls["n"] == 1
|
||
|
||
store.values["AIA_SKILL_DISABLED_NAMES"] = "[\"weather\"]"
|
||
_ = sp.get_executor_prompt_static(
|
||
store=store,
|
||
tools=reg, # type: ignore[arg-type]
|
||
base_url="",
|
||
base_system="base",
|
||
workspace_dir=None,
|
||
skill_binding_role="generalist",
|
||
)
|
||
assert calls["n"] == 2
|
||
|
||
|
||
def test_workspace_skills_layout_signature_changes_on_skill_md_edit(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||
root = tmp_path / "skills_root_sig"
|
||
(root / "pkg").mkdir(parents=True)
|
||
f = root / "pkg" / "SKILL.md"
|
||
f.write_text("---\nname: x\ndescription: d\n---\n", encoding="utf-8")
|
||
monkeypatch.setenv("AIA_SKILLS_ROOT", str(root))
|
||
a = workspace_skills_layout_signature()
|
||
f.write_text("---\nname: x\ndescription: d\n---\n\nbody", encoding="utf-8")
|
||
b = workspace_skills_layout_signature()
|
||
assert a != b
|
||
|
||
|
||
def test_executor_static_prompt_cache_invalidates_on_workspace_skills_layout_change(monkeypatch: pytest.MonkeyPatch) -> None:
|
||
from runtime import system_prompt as sp
|
||
|
||
class DummyStore:
|
||
def get_setting(self, key: str) -> str:
|
||
_ = key
|
||
return ""
|
||
|
||
class DummyReg:
|
||
pass
|
||
|
||
calls = {"n": 0}
|
||
sig = {"v": "layout-a"}
|
||
|
||
def _skills_block(**kwargs) -> str:
|
||
_ = kwargs
|
||
calls["n"] += 1
|
||
return "skills-block"
|
||
|
||
monkeypatch.setattr(sp, "expert_workspace_signature_token", lambda: ("sig",))
|
||
monkeypatch.setattr(sp, "workspace_skills_layout_signature", lambda: sig["v"])
|
||
monkeypatch.setattr(sp, "build_project_context_block", lambda **kwargs: "")
|
||
monkeypatch.setattr(sp, "build_skills_catalog_block", _skills_block)
|
||
monkeypatch.setattr(
|
||
sp,
|
||
"render_runtime_prompt",
|
||
lambda prompt_id, variables, strict: f"{prompt_id}\n{variables.get('skills_catalog') or ''}",
|
||
)
|
||
|
||
store = DummyStore()
|
||
reg = DummyReg()
|
||
_ = sp.get_executor_prompt_static(
|
||
store=store,
|
||
tools=reg, # type: ignore[arg-type]
|
||
base_url="",
|
||
base_system="base",
|
||
workspace_dir=None,
|
||
skill_binding_role="generalist",
|
||
)
|
||
_ = sp.get_executor_prompt_static(
|
||
store=store,
|
||
tools=reg, # type: ignore[arg-type]
|
||
base_url="",
|
||
base_system="base",
|
||
workspace_dir=None,
|
||
skill_binding_role="generalist",
|
||
)
|
||
assert calls["n"] == 1
|
||
|
||
sig["v"] = "layout-b"
|
||
_ = sp.get_executor_prompt_static(
|
||
store=store,
|
||
tools=reg, # type: ignore[arg-type]
|
||
base_url="",
|
||
base_system="base",
|
||
workspace_dir=None,
|
||
skill_binding_role="generalist",
|
||
)
|
||
assert calls["n"] == 2
|
||
|
||
|
||
def test_executor_static_prompt_cache_invalidates_on_skill_role_binding_change(monkeypatch: pytest.MonkeyPatch) -> None:
|
||
from runtime import system_prompt as sp
|
||
|
||
class DummyStore:
|
||
def __init__(self) -> None:
|
||
self.values: dict[str, str] = {"skill_role_binding": "{}"}
|
||
|
||
def get_setting(self, key: str) -> str:
|
||
return str(self.values.get(key, ""))
|
||
|
||
class DummyReg:
|
||
pass
|
||
|
||
calls = {"n": 0}
|
||
|
||
def _skills_block(**kwargs) -> str:
|
||
_ = kwargs
|
||
calls["n"] += 1
|
||
return "skills-block"
|
||
|
||
monkeypatch.setattr(sp, "expert_workspace_signature_token", lambda: ("sig",))
|
||
monkeypatch.setattr(sp, "build_project_context_block", lambda **kwargs: "")
|
||
monkeypatch.setattr(sp, "build_skills_catalog_block", _skills_block)
|
||
monkeypatch.setattr(
|
||
sp,
|
||
"render_runtime_prompt",
|
||
lambda prompt_id, variables, strict: f"{prompt_id}\n{variables.get('skills_catalog') or ''}",
|
||
)
|
||
|
||
store = DummyStore()
|
||
reg = DummyReg()
|
||
_ = sp.get_executor_prompt_static(
|
||
store=store,
|
||
tools=reg, # type: ignore[arg-type]
|
||
base_url="",
|
||
base_system="base",
|
||
workspace_dir=None,
|
||
skill_binding_role="generalist",
|
||
)
|
||
_ = sp.get_executor_prompt_static(
|
||
store=store,
|
||
tools=reg, # type: ignore[arg-type]
|
||
base_url="",
|
||
base_system="base",
|
||
workspace_dir=None,
|
||
skill_binding_role="generalist",
|
||
)
|
||
assert calls["n"] == 1
|
||
|
||
store.values["skill_role_binding"] = '{"generalist": ["alpha-skill"]}'
|
||
_ = sp.get_executor_prompt_static(
|
||
store=store,
|
||
tools=reg, # type: ignore[arg-type]
|
||
base_url="",
|
||
base_system="base",
|
||
workspace_dir=None,
|
||
skill_binding_role="generalist",
|
||
)
|
||
assert calls["n"] == 2
|
||
|
||
|
||
def test_executor_static_prompt_cache_invalidates_on_workspace_revision_change(monkeypatch: pytest.MonkeyPatch) -> None:
|
||
from runtime import system_prompt as sp
|
||
|
||
class DummyStore:
|
||
def get_setting(self, key: str) -> str:
|
||
_ = key
|
||
return ""
|
||
|
||
class DummyReg:
|
||
pass
|
||
|
||
calls = {"n": 0}
|
||
token = {"v": 1}
|
||
|
||
def _skills_block(**kwargs) -> str:
|
||
_ = kwargs
|
||
calls["n"] += 1
|
||
return "skills-block"
|
||
|
||
monkeypatch.setattr(sp, "expert_workspace_signature_token", lambda: ("revision", token["v"]))
|
||
monkeypatch.setattr(sp, "build_project_context_block", lambda **kwargs: "")
|
||
monkeypatch.setattr(sp, "build_skills_catalog_block", _skills_block)
|
||
monkeypatch.setattr(
|
||
sp,
|
||
"render_runtime_prompt",
|
||
lambda prompt_id, variables, strict: f"{prompt_id}\n{variables.get('skills_catalog') or ''}",
|
||
)
|
||
|
||
store = DummyStore()
|
||
reg = DummyReg()
|
||
_ = sp.get_executor_prompt_static(
|
||
store=store,
|
||
tools=reg, # type: ignore[arg-type]
|
||
base_url="",
|
||
base_system="base",
|
||
workspace_dir=None,
|
||
skill_binding_role="generalist",
|
||
)
|
||
_ = sp.get_executor_prompt_static(
|
||
store=store,
|
||
tools=reg, # type: ignore[arg-type]
|
||
base_url="",
|
||
base_system="base",
|
||
workspace_dir=None,
|
||
skill_binding_role="generalist",
|
||
)
|
||
assert calls["n"] == 1
|
||
|
||
token["v"] = 2
|
||
_ = sp.get_executor_prompt_static(
|
||
store=store,
|
||
tools=reg, # type: ignore[arg-type]
|
||
base_url="",
|
||
base_system="base",
|
||
workspace_dir=None,
|
||
skill_binding_role="generalist",
|
||
)
|
||
assert calls["n"] == 2
|