| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449 |
- """Functional tests for toyclaw components (no API key needed)."""
- import asyncio
- import json
- import os
- import shutil
- import sys
- import tempfile
- from pathlib import Path
- loop = asyncio.new_event_loop()
- asyncio.set_event_loop(loop)
- # ── 1. Config ──
- from toyclaw.config import Config, load_config
- def test_config():
- # Default config
- cfg = Config()
- assert cfg.model == "gpt-4o"
- assert cfg.max_iterations == 30
- assert cfg.heartbeat_interval == 1800
- # Load from a temp JSON file
- tmpdir = tempfile.mkdtemp(prefix="oc_cfg_")
- try:
- p = Path(tmpdir) / "config.json"
- p.write_text(json.dumps({"api_key": "sk-test", "model": "gpt-3.5-turbo"}))
- cfg = load_config(p)
- assert cfg.api_key == "sk-test"
- assert cfg.model == "gpt-3.5-turbo"
- # Defaults still applied for missing keys
- assert cfg.max_iterations == 30
- print(" config: Config + load_config OK")
- finally:
- shutil.rmtree(tmpdir)
- # ── 2. Provider data classes ──
- from toyclaw.provider import LLMResponse, ToolCallRequest
- def test_provider_dataclasses():
- tc = ToolCallRequest(id="t1", name="read_file", arguments={"path": "x"})
- assert tc.id == "t1" and tc.name == "read_file"
- assert tc.arguments == {"path": "x"}
- resp = LLMResponse(content="hi", tool_calls=[], finish_reason="stop")
- assert resp.content == "hi"
- assert not resp.has_tool_calls
- resp2 = LLMResponse(tool_calls=[tc])
- assert resp2.has_tool_calls
- print(" provider: dataclasses OK")
- # ── 3. Tool registry ──
- from toyclaw.tools.base import ToolRegistry
- def test_tool_registry():
- reg = ToolRegistry()
- assert reg.get_definitions() == []
- assert reg.names == []
- # Execute on missing tool
- result = loop.run_until_complete(reg.execute("nope", {}))
- assert "not found" in result.lower()
- print(" tools.base: ToolRegistry OK")
- # ── 4. Builtin tools ──
- from toyclaw.tools.builtin import (
- EditFileTool,
- ListDirTool,
- ReadFileTool,
- ShellTool,
- WriteFileTool,
- )
- def test_builtin_tools():
- tmpdir = Path(tempfile.mkdtemp(prefix="oc_test_"))
- ws = tmpdir # workspace
- try:
- # ReadFile — missing file
- rt = ReadFileTool(workspace=ws)
- result = loop.run_until_complete(rt.execute(path=str(tmpdir / "nope.txt")))
- assert "not found" in result.lower()
- # WriteFile
- wt = WriteFileTool(workspace=ws)
- wf = tmpdir / "out.txt"
- result = loop.run_until_complete(wt.execute(path=str(wf), content="wrote this"))
- assert wf.exists()
- assert wf.read_text() == "wrote this"
- print(" builtin: WriteFileTool OK")
- # ReadFile — existing file
- result = loop.run_until_complete(rt.execute(path=str(wf)))
- assert "wrote this" in result
- print(" builtin: ReadFileTool OK")
- # EditFile
- et = EditFileTool(workspace=ws)
- result = loop.run_until_complete(et.execute(path=str(wf), old_text="wrote", new_text="edited"))
- assert "edited this" in wf.read_text()
- print(" builtin: EditFileTool OK")
- # EditFile — old_text not found
- result = loop.run_until_complete(et.execute(path=str(wf), old_text="NOPE", new_text="x"))
- assert "not found" in result.lower()
- print(" builtin: EditFileTool error case OK")
- # ListDir
- ld = ListDirTool(workspace=ws)
- result = loop.run_until_complete(ld.execute(path=str(tmpdir)))
- assert "out.txt" in result
- print(" builtin: ListDirTool OK")
- # ShellTool
- st = ShellTool(workspace=ws)
- result = loop.run_until_complete(st.execute(command="echo test123"))
- assert "test123" in result
- print(" builtin: ShellTool OK")
- # ShellTool — deny pattern
- result = loop.run_until_complete(st.execute(command="rm -rf /"))
- assert "blocked" in result.lower()
- print(" builtin: ShellTool safety guard OK")
- finally:
- shutil.rmtree(tmpdir)
- # ── 5. Tool registry integration ──
- def test_tool_registry_with_tools():
- tmpdir = Path(tempfile.mkdtemp(prefix="oc_reg_"))
- try:
- reg = ToolRegistry()
- reg.register(ReadFileTool(workspace=tmpdir))
- reg.register(WriteFileTool(workspace=tmpdir))
- reg.register(ListDirTool(workspace=tmpdir))
- assert len(reg.names) == 3
- assert "read_file" in reg.names
- # Schema structure
- schemas = reg.get_definitions()
- assert len(schemas) == 3
- assert all(s["type"] == "function" for s in schemas)
- assert schemas[0]["function"]["name"] in ("read_file", "write_file", "list_dir")
- print(" registry: register + schema OK")
- # Execute via registry
- loop.run_until_complete(reg.execute("write_file", {"path": str(tmpdir / "hi.txt"), "content": "hi"}))
- result = loop.run_until_complete(reg.execute("read_file", {"path": str(tmpdir / "hi.txt")}))
- assert "hi" in result
- print(" registry: execute integration OK")
- finally:
- shutil.rmtree(tmpdir)
- # ── 6. Session ──
- from toyclaw.session import Session, SessionManager
- def test_session():
- tmpdir = Path(tempfile.mkdtemp(prefix="oc_sess_"))
- try:
- mgr = SessionManager(workspace=tmpdir)
- # New session
- sess = mgr.get_or_create("test_conv")
- assert isinstance(sess, Session)
- assert sess.key == "test_conv"
- assert len(sess.messages) == 0
- # Add messages
- sess.messages.append({"role": "user", "content": "hi"})
- sess.messages.append({"role": "assistant", "content": "hello"})
- sess.messages.append({"role": "user", "content": "q2"})
- mgr.save(sess)
- # get_history trims to user turn
- hist = sess.get_history()
- assert hist[0]["role"] == "user"
- assert len(hist) == 3
- print(" session: message append + get_history OK")
- # Persistence — reload from disk
- mgr2 = SessionManager(workspace=tmpdir)
- sess2 = mgr2.get_or_create("test_conv")
- assert len(sess2.messages) == 3
- assert sess2.messages[0]["content"] == "hi"
- print(" session: persistence OK")
- finally:
- shutil.rmtree(tmpdir)
- # ── 7. Skills ──
- from toyclaw.skills import SkillsLoader
- def test_skills():
- tmpdir = Path(tempfile.mkdtemp(prefix="oc_skills_"))
- try:
- # No skills dir
- loader = SkillsLoader(tmpdir)
- assert loader.list_skills() == []
- # Create a fake skill
- sk = tmpdir / "skills" / "demo"
- sk.mkdir(parents=True)
- (sk / "SKILL.md").write_text("# Demo Skill\nDoes demo things.\n\nMore detail here.")
- found = loader.list_skills()
- assert len(found) == 1
- assert found[0]["name"] == "demo"
- assert "Does demo things" in found[0]["description"]
- # build_summary
- summary = loader.build_summary()
- assert "demo" in summary
- assert "Does demo things" in summary
- print(" skills: SkillsLoader OK")
- finally:
- shutil.rmtree(tmpdir)
- # ── 8. Context ──
- from toyclaw.context import ContextBuilder
- def test_context():
- tmpdir = Path(tempfile.mkdtemp(prefix="oc_ctx_"))
- try:
- # Bootstrap file
- (tmpdir / "AGENTS.md").write_text("# Test Agent Instructions")
- builder = ContextBuilder(tmpdir)
- messages = builder.build_messages(
- history=[],
- user_message="hello",
- skills_summary="- **test_skill**: does stuff",
- )
- # Should have system + user
- assert len(messages) == 2
- assert messages[0]["role"] == "system"
- assert messages[1]["role"] == "user"
- system = messages[0]["content"]
- assert "ToyClaw" in system
- assert "Test Agent Instructions" in system
- assert "test_skill" in system
- print(" context: ContextBuilder OK")
- # add_assistant_message / add_tool_result
- ContextBuilder.add_assistant_message(messages, "thinking...")
- assert messages[-1]["role"] == "assistant"
- ContextBuilder.add_tool_result(messages, "tc1", "read_file", "file content")
- assert messages[-1]["role"] == "tool"
- assert messages[-1]["tool_call_id"] == "tc1"
- print(" context: message helpers OK")
- # strip_runtime_tag
- raw = messages[1]["content"]
- assert "[Runtime Context]" in raw
- stripped = ContextBuilder.strip_runtime_tag(raw)
- assert stripped == "hello"
- print(" context: strip_runtime_tag OK")
- finally:
- shutil.rmtree(tmpdir)
- # ── 9. Cron ──
- from toyclaw.cron import CronJob, CronSchedule, CronService
- def test_cron():
- tmpdir = Path(tempfile.mkdtemp(prefix="oc_cron_"))
- try:
- store = tmpdir / "cron.json"
- svc = CronService(store_path=store)
- # Add a recurring job
- sched = CronSchedule(kind="every", every_ms=30_000)
- job = svc.add_job(name="test-job", schedule=sched, message="say hi")
- assert isinstance(job, CronJob)
- assert job.id
- assert job.name == "test-job"
- # List
- jobs = svc.list_jobs()
- assert len(jobs) == 1
- assert jobs[0].id == job.id
- print(" cron: add_job + list_jobs OK")
- # Persistence
- assert store.exists()
- data = json.loads(store.read_text())
- assert len(data) == 1
- print(" cron: JSON persistence OK")
- # Remove
- ok = svc.remove_job(job.id)
- assert ok
- assert len(svc.list_jobs()) == 0
- # Remove non-existent
- ok = svc.remove_job("nope")
- assert not ok
- print(" cron: remove_job OK")
- # One-shot schedule
- import time
- future_ms = int(time.time() * 1000) + 60_000
- sched2 = CronSchedule(kind="at", at_ms=future_ms)
- job2 = svc.add_job(name="once", schedule=sched2, message="one time", delete_after_run=True)
- assert job2.delete_after_run
- svc.remove_job(job2.id)
- # Cron expression
- sched3 = CronSchedule(kind="cron", expr="0 9 * * *")
- job3 = svc.add_job(name="daily", schedule=sched3, message="good morning")
- assert job3.next_run_ms is not None # croniter computed it
- svc.remove_job(job3.id)
- print(" cron: all schedule types OK")
- finally:
- shutil.rmtree(tmpdir)
- # ── 10. CronTool ──
- from toyclaw.tools.builtin import CronTool
- def test_cron_tool():
- tmpdir = Path(tempfile.mkdtemp(prefix="oc_ct_"))
- try:
- svc = CronService(store_path=tmpdir / "cron.json")
- ct = CronTool(service=svc)
- ct.set_context("cli", "test")
- # Add via tool
- result = loop.run_until_complete(ct.execute(action="add", message="remind me", every_seconds=60))
- assert "created" in result.lower()
- print(" cron_tool: add OK")
- # List via tool
- result = loop.run_until_complete(ct.execute(action="list"))
- assert "remind me" in result
- print(" cron_tool: list OK")
- # Remove via tool
- job_id = svc.list_jobs()[0].id
- result = loop.run_until_complete(ct.execute(action="remove", job_id=job_id))
- assert "removed" in result.lower()
- print(" cron_tool: remove OK")
- # Error: missing message
- result = loop.run_until_complete(ct.execute(action="add"))
- assert "error" in result.lower()
- print(" cron_tool: error handling OK")
- finally:
- shutil.rmtree(tmpdir)
- # ── 11. Heartbeat ──
- from toyclaw.heartbeat import HeartbeatService
- def test_heartbeat():
- tmpdir = Path(tempfile.mkdtemp(prefix="oc_hb_"))
- try:
- # HeartbeatService requires a provider; use a mock
- class FakeProvider:
- pass
- hb = HeartbeatService(
- workspace=tmpdir,
- provider=FakeProvider(),
- interval_s=1800,
- enabled=False,
- )
- assert hb.interval_s == 1800
- assert not hb.enabled
- assert hb._file == tmpdir / "HEARTBEAT.md"
- print(" heartbeat: init OK")
- # With HEARTBEAT.md
- (tmpdir / "HEARTBEAT.md").write_text("- Check email\n- Review tasks")
- assert hb._file.exists()
- print(" heartbeat: HEARTBEAT.md detected OK")
- finally:
- shutil.rmtree(tmpdir)
- # ── 12. WebFetchTool (strip HTML) ──
- from toyclaw.tools.builtin import WebFetchTool
- def test_web_fetch_strip():
- wf = WebFetchTool()
- html = "<html><body><h1>Title</h1><p>Hello <b>world</b></p></body></html>"
- text = wf._strip(html)
- assert "Title" in text
- assert "Hello" in text
- assert "world" in text
- assert "<" not in text # all tags stripped
- print(" web_fetch: HTML strip OK")
- # ── Run all ──
- if __name__ == "__main__":
- tests = [
- test_config,
- test_provider_dataclasses,
- test_tool_registry,
- test_builtin_tools,
- test_tool_registry_with_tools,
- test_session,
- test_skills,
- test_context,
- test_cron,
- test_cron_tool,
- test_heartbeat,
- test_web_fetch_strip,
- ]
- passed = 0
- failed = 0
- for t in tests:
- name = t.__name__
- try:
- t()
- passed += 1
- except Exception as e:
- print(f" FAIL {name}: {e}")
- import traceback
- traceback.print_exc()
- failed += 1
- print(f"\n{'=' * 50}")
- print(f"Results: {passed} passed, {failed} failed out of {len(tests)}")
- if failed:
- sys.exit(1)
- else:
- print("All tests passed!")
|