hkuds--openharness
133 行
4.5 KiB
Python
133 行
4.5 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from openharness.api.client import ApiMessageCompleteEvent, SupportsStreamingMessages
|
|
from openharness.api.usage import UsageSnapshot
|
|
from openharness.config.settings import Settings, save_settings
|
|
from openharness.engine.messages import ConversationMessage, TextBlock
|
|
from openharness.engine.query_engine import QueryEngine
|
|
from openharness.hooks import HookEvent, HookExecutionContext, HookExecutor, load_hook_registry
|
|
from openharness.hooks.schemas import CommandHookDefinition
|
|
from openharness.permissions.checker import PermissionChecker
|
|
from openharness.tools.base import ToolRegistry
|
|
from openharness.ui.runtime import build_runtime
|
|
|
|
|
|
class CapturingApiClient(SupportsStreamingMessages):
|
|
def __init__(self) -> None:
|
|
self.requests = []
|
|
|
|
async def stream_message(self, request):
|
|
self.requests.append(request)
|
|
yield ApiMessageCompleteEvent(
|
|
message=ConversationMessage(role="assistant", content=[TextBlock(text="done")]),
|
|
usage=UsageSnapshot(),
|
|
)
|
|
|
|
|
|
def _python_json_command(code: str) -> str:
|
|
return f"{sys.executable} -c {json.dumps(code)}"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_command_hook_can_receive_payload_on_stdin(tmp_path: Path) -> None:
|
|
seen = tmp_path / "seen.json"
|
|
code = (
|
|
"import json, pathlib, sys; "
|
|
f"pathlib.Path({str(seen)!r}).write_text(sys.stdin.read(), encoding='utf-8'); "
|
|
"print(json.dumps({'additional_context': 'stdin ok'}))"
|
|
)
|
|
registry = load_hook_registry(
|
|
Settings(
|
|
hooks={
|
|
HookEvent.USER_PROMPT_SUBMIT.value: [
|
|
CommandHookDefinition(command=_python_json_command(code), stdin_payload=True)
|
|
]
|
|
}
|
|
)
|
|
)
|
|
client = CapturingApiClient()
|
|
executor = HookExecutor(
|
|
registry,
|
|
HookExecutionContext(cwd=tmp_path, api_client=client, default_model="test-model"),
|
|
)
|
|
|
|
result = await executor.execute(HookEvent.USER_PROMPT_SUBMIT, {"prompt": "hello"})
|
|
|
|
assert json.loads(seen.read_text(encoding="utf-8"))["prompt"] == "hello"
|
|
assert result.additional_context == "stdin ok"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_user_prompt_hook_context_is_injected_for_one_turn(tmp_path: Path) -> None:
|
|
code = "import json; print(json.dumps({'additional_context': 'CLAUDE_MEM_CONTEXT'}))"
|
|
registry = load_hook_registry(
|
|
Settings(
|
|
hooks={
|
|
HookEvent.USER_PROMPT_SUBMIT.value: [
|
|
CommandHookDefinition(command=_python_json_command(code))
|
|
]
|
|
}
|
|
)
|
|
)
|
|
client = CapturingApiClient()
|
|
engine = QueryEngine(
|
|
api_client=client,
|
|
tool_registry=ToolRegistry(),
|
|
permission_checker=PermissionChecker(Settings().permission),
|
|
cwd=tmp_path,
|
|
model="test-model",
|
|
system_prompt="system",
|
|
hook_executor=HookExecutor(
|
|
registry,
|
|
HookExecutionContext(cwd=tmp_path, api_client=client, default_model="test-model"),
|
|
),
|
|
tool_metadata={"session_id": "s1"},
|
|
)
|
|
|
|
events = [event async for event in engine.submit_message("hello")]
|
|
|
|
assert events
|
|
assert any("CLAUDE_MEM_CONTEXT" in message.text for message in client.requests[0].messages)
|
|
assert all("CLAUDE_MEM_CONTEXT" not in message.text for message in engine.messages)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_session_start_hook_context_is_added_to_runtime_system_prompt(tmp_path: Path) -> None:
|
|
code = "import json; print(json.dumps({'additional_context': 'SESSION_MEMORY_CONTEXT'}))"
|
|
settings = Settings(
|
|
provider="anthropic",
|
|
model="test-model",
|
|
hooks={
|
|
HookEvent.SESSION_START.value: [
|
|
CommandHookDefinition(command=_python_json_command(code))
|
|
]
|
|
},
|
|
)
|
|
config_dir = tmp_path / "config"
|
|
config_dir.mkdir()
|
|
save_settings(settings, config_dir / "settings.json")
|
|
|
|
import os
|
|
|
|
old_config_dir = os.environ.get("OPENHARNESS_CONFIG_DIR")
|
|
os.environ["OPENHARNESS_CONFIG_DIR"] = str(config_dir)
|
|
try:
|
|
bundle = await build_runtime(
|
|
cwd=str(tmp_path),
|
|
api_client=CapturingApiClient(),
|
|
include_project_memory=False,
|
|
)
|
|
finally:
|
|
if old_config_dir is None:
|
|
os.environ.pop("OPENHARNESS_CONFIG_DIR", None)
|
|
else:
|
|
os.environ["OPENHARNESS_CONFIG_DIR"] = old_config_dir
|
|
|
|
assert "SESSION_MEMORY_CONTEXT" in bundle.engine.system_prompt
|