logic-engine / tests /test_openclaw.py
ghostdrive1's picture
Upload folder using huggingface_hub
116524e verified
Raw
History Blame Contribute Delete
10.5 kB
"""Tests for OpenClaw integration β€” OpenClawToTraceStep and end-to-end pipeline."""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any, Optional
import pytest
from ace.core.context import ACEStepContext, SkillbookView
from ace.core.outputs import (
AgentOutput,
ReflectorOutput,
SkillManagerOutput,
)
from ace.core.skillbook import Skillbook, UpdateBatch, UpdateOperation
from ace.integrations.openclaw import OpenClawToTraceStep
from ace.steps import learning_tail
from ace.steps.load_traces import LoadTracesStep
from pipeline import Pipeline
# ------------------------------------------------------------------ #
# Helpers β€” mock roles
# ------------------------------------------------------------------ #
class MockReflector:
"""Minimal mock satisfying ReflectorLike."""
def __init__(self, output: ReflectorOutput | None = None):
self.output = output or ReflectorOutput(
reasoning="test reasoning",
correct_approach="test approach",
key_insight="test insight",
)
self.calls: list[dict] = []
def reflect(
self,
*,
question: str,
agent_output: AgentOutput,
skillbook: Any,
ground_truth: Optional[str] = None,
feedback: Optional[str] = None,
**kwargs: Any,
) -> ReflectorOutput:
self.calls.append(
{
"question": question,
"agent_output": agent_output,
"ground_truth": ground_truth,
"feedback": feedback,
**kwargs,
}
)
return self.output
class MockSkillManager:
"""Minimal mock satisfying SkillManagerLike.
The real SM mutates the skillbook directly via tool calls; this mock
applies its pre-canned ``output`` to the incoming skillbook so
``UpdateStep`` behaves like the live code path.
"""
def __init__(self, output: SkillManagerOutput | None = None):
self.output = output or SkillManagerOutput(
update=UpdateBatch(reasoning="test", operations=[]),
)
self.calls: list[dict] = []
def update_skills(
self,
*,
reflections: tuple[ReflectorOutput, ...],
skillbook: Any,
question_context: str,
progress: str,
**kwargs: Any,
) -> SkillManagerOutput:
self.calls.append(
{
"reflections": reflections,
"question_context": question_context,
"progress": progress,
}
)
skillbook.apply_update(self.output.update)
return self.output
# ------------------------------------------------------------------ #
# Fixtures
# ------------------------------------------------------------------ #
@pytest.fixture
def sample_jsonl(tmp_path: Path) -> Path:
"""Create a minimal OpenClaw session JSONL file."""
events = [
{
"type": "session",
"id": "s1",
"timestamp": "2026-01-01T00:00:00Z",
"version": 1,
"cwd": "/app",
},
{
"type": "message",
"id": "m1",
"parentId": "s1",
"timestamp": "2026-01-01T00:00:01Z",
"message": {
"role": "user",
"content": [{"type": "text", "text": "Hello, help me debug this."}],
},
},
{
"type": "message",
"id": "m2",
"parentId": "m1",
"timestamp": "2026-01-01T00:00:02Z",
"message": {
"role": "assistant",
"content": [
{"type": "thinking", "thinking": "Let me analyze the issue..."},
{"type": "text", "text": "I'll help you debug this."},
{
"type": "toolCall",
"id": "tc1",
"name": "Read",
"arguments": {"file_path": "/app/main.py"},
},
],
},
},
{
"type": "message",
"id": "m3",
"parentId": "m2",
"timestamp": "2026-01-01T00:00:03Z",
"message": {
"role": "toolResult",
"content": [
{"type": "text", "text": "def main():\n print('hello')"}
],
},
},
]
path = tmp_path / "test-session.jsonl"
path.write_text("\n".join(json.dumps(e) for e in events) + "\n")
return path
# ------------------------------------------------------------------ #
# OpenClawToTraceStep tests
# ------------------------------------------------------------------ #
class TestOpenClawToTraceStep:
def test_requires_provides(self):
step = OpenClawToTraceStep()
assert step.requires == frozenset({"trace"})
assert step.provides == frozenset({"trace"})
def test_converts_to_trace_dict(self):
"""Step should convert raw events into a structured trace dict."""
raw_events = [
{"type": "session", "id": "s1", "cwd": "/app"},
{
"type": "message",
"id": "m1",
"message": {
"role": "user",
"content": [{"type": "text", "text": "Hello"}],
},
},
{
"type": "message",
"id": "m2",
"message": {
"role": "assistant",
"content": [{"type": "text", "text": "Hi there"}],
},
},
]
ctx = ACEStepContext(trace=raw_events)
result = OpenClawToTraceStep()(ctx)
trace = result.trace
assert isinstance(trace, dict)
assert trace["question"] == "User: Hello"
assert trace["answer"] == "Hi there"
assert trace["skill_ids"] == []
assert trace["ground_truth"] is None
assert "reasoning" in trace
assert "feedback" in trace
def test_none_trace(self):
"""Step should handle None trace gracefully."""
ctx = ACEStepContext(trace=None)
result = OpenClawToTraceStep()(ctx)
assert result.trace is None
def test_empty_list_trace(self):
"""Step should handle empty list trace gracefully."""
ctx = ACEStepContext(trace=[])
result = OpenClawToTraceStep()(ctx)
assert result.trace == []
# ------------------------------------------------------------------ #
# End-to-end: LoadTracesStep β†’ OpenClawToTraceStep β†’ learning_tail
# ------------------------------------------------------------------ #
class TestOpenClawEndToEnd:
def test_load_and_convert(self, sample_jsonl: Path):
"""LoadTracesStep β†’ OpenClawToTraceStep should produce trace data."""
load_step = LoadTracesStep()
convert_step = OpenClawToTraceStep()
ctx = ACEStepContext(sample=str(sample_jsonl))
ctx = load_step(ctx)
assert isinstance(ctx.trace, list)
assert len(ctx.trace) == 4
ctx = convert_step(ctx)
# Converted to structured trace dict
assert isinstance(ctx.trace, dict)
assert "question" in ctx.trace
assert "reasoning" in ctx.trace
assert "answer" in ctx.trace
assert ctx.trace["skill_ids"] == []
assert ctx.trace["ground_truth"] is None
def test_full_pipeline_with_mocks(self, sample_jsonl: Path):
"""Full pipeline: load β†’ convert β†’ reflect β†’ tag β†’ update β†’ apply."""
reflector = MockReflector()
skill_manager = MockSkillManager()
skillbook = Skillbook()
load_step = LoadTracesStep()
convert_step = OpenClawToTraceStep()
steps = [
load_step,
convert_step,
*learning_tail(reflector, skill_manager, skillbook),
]
pipeline = Pipeline(steps)
ctx = ACEStepContext(
sample=str(sample_jsonl),
skillbook=SkillbookView(skillbook),
)
result = pipeline.run([ctx])
pipeline.wait_for_background()
assert len(result) == 1
assert len(reflector.calls) == 1
assert len(skill_manager.calls) == 1
def test_pipeline_with_add_operation(self, sample_jsonl: Path):
"""Pipeline with a SkillManager that adds a skill."""
add_op = UpdateOperation(
type="ADD",
section="debugging",
issue="Use structured logging for better debug traces",
insight="Use structured logging for better debug traces",
skill_id=None,
metadata={"helpful": 1, "harmful": 0, "neutral": 0},
)
sm_output = SkillManagerOutput(
update=UpdateBatch(reasoning="Found useful pattern", operations=[add_op]),
)
reflector = MockReflector()
skill_manager = MockSkillManager(output=sm_output)
skillbook = Skillbook()
steps = [
LoadTracesStep(),
OpenClawToTraceStep(),
*learning_tail(reflector, skill_manager, skillbook),
]
pipeline = Pipeline(steps)
ctx = ACEStepContext(
sample=str(sample_jsonl),
skillbook=SkillbookView(skillbook),
)
pipeline.run([ctx])
pipeline.wait_for_background()
# Skillbook should now have one skill (legacy "debugging" β†’ "context")
assert len(skillbook.skills()) == 1
skill = skillbook.skills()[0]
assert skill.section == "context"
assert "structured logging" in skill.insight
def test_empty_session_skipped(self, tmp_path: Path):
"""Empty JSONL should produce empty trace."""
path = tmp_path / "empty.jsonl"
path.write_text("")
load_step = LoadTracesStep()
convert_step = OpenClawToTraceStep()
ctx = ACEStepContext(sample=str(path))
ctx = load_step(ctx)
assert ctx.trace == []
ctx = convert_step(ctx)
assert ctx.trace == []