main
py 163 lines 5.03 KB
Raw
1 import asyncio
2 import sys
3 from pathlib import Path
4 from types import SimpleNamespace
5
6 PROJECT_ROOT = Path(__file__).resolve().parents[1]
7 if str(PROJECT_ROOT) not in sys.path:
8 sys.path.insert(0, str(PROJECT_ROOT))
9
10 from plugins._skills.api import skills_catalog
11
12
13 class FakeHistory:
14 def __init__(self):
15 self.messages = []
16
17 def output(self):
18 return self.messages
19
20
21 class FakeContext:
22 def __init__(self):
23 self.id = "ctx"
24 self.data = {}
25 self.agent = FakeAgent(self)
26
27 def get_agent(self):
28 return self.agent
29
30 def get_data(self, key, recursive=True):
31 return self.data.get(key)
32
33 def set_data(self, key, value, recursive=True):
34 self.data[key] = value
35
36
37 class FakeAgent:
38 def __init__(self, context):
39 self.context = context
40 self.history = FakeHistory()
41 self.tool_results = []
42
43 def hist_add_tool_result(self, tool_name, tool_result, **kwargs):
44 content = {"tool_name": tool_name, "tool_result": tool_result, **kwargs}
45 message = {"ai": False, "content": content}
46 self.tool_results.append(content)
47 self.history.messages.append(message)
48 return SimpleNamespace(output=lambda: [message])
49
50
51 def _patch_catalog(monkeypatch, context):
52 skill = {
53 "name": "demo-skill",
54 "description": "Demo skill.",
55 "path": "/a0/skills/demo-skill",
56 "origin": "Built-in",
57 "hidden": False,
58 }
59
60 monkeypatch.setattr(
61 skills_catalog.AgentContext,
62 "get",
63 staticmethod(lambda context_id: context if context_id == context.id else None),
64 )
65 monkeypatch.setattr(
66 skills_catalog.projects,
67 "get_context_project_name",
68 lambda _context: "",
69 )
70 monkeypatch.setattr(
71 skills_catalog.skills,
72 "list_skill_catalog",
73 lambda *args, **kwargs: [skill],
74 )
75 monkeypatch.setattr(
76 skills_catalog.skills,
77 "load_skill_for_agent",
78 lambda skill_name, agent: f"Skill: {skill_name}\n\nInstructions:\nUse it.",
79 )
80 monkeypatch.setattr(
81 skills_catalog.skills,
82 "add_loaded_skill_name",
83 lambda agent, skill_name: agent.context.set_data("loaded_skills", [skill_name]),
84 )
85 monkeypatch.setattr(
86 skills_catalog.skills,
87 "get_loaded_skill_entries",
88 lambda agent: [
89 {"name": name} for name in (agent.context.get_data("loaded_skills") or [])
90 ] if agent else [],
91 )
92 monkeypatch.setattr(skills_catalog.skills, "get_scope_active_skills", lambda agent: [])
93 monkeypatch.setattr(skills_catalog.skills, "get_scope_hidden_skills", lambda agent: [])
94 monkeypatch.setattr(skills_catalog.skills, "get_chat_active_skills", lambda context: [])
95 monkeypatch.setattr(skills_catalog.skills, "get_chat_disabled_skills", lambda context: [])
96 monkeypatch.setattr(skills_catalog.skills, "get_chat_visible_skills", lambda context: [])
97 monkeypatch.setattr(skills_catalog.skills, "get_hidden_skills", lambda agent: [])
98 monkeypatch.setattr(skills_catalog.skills, "get_max_active_skills", lambda **kwargs: 20)
99
100 saved = []
101 monkeypatch.setattr(skills_catalog, "save_tmp_chat", lambda ctx: saved.append(ctx.id))
102 return saved
103
104
105 def test_skills_catalog_activate_loads_skill_into_chat_history(monkeypatch):
106 context = FakeContext()
107 saved = _patch_catalog(monkeypatch, context)
108 handler = skills_catalog.SkillsCatalog(None, None)
109
110 response = asyncio.run(
111 handler.process(
112 {
113 "action": "activate",
114 "context_id": "ctx",
115 "skill": {"name": "demo-skill", "path": "/a0/skills/demo-skill"},
116 },
117 None,
118 )
119 )
120
121 assert response["ok"] is True, response
122 assert context.get_data("loaded_skills") == ["demo-skill"]
123 assert len(context.agent.tool_results) == 1
124 assert "Skill: demo-skill" in context.agent.tool_results[0]["tool_result"]
125 assert response["active_skills"][0]["state_source"] == "Loaded in chat history"
126 assert saved == ["ctx"]
127
128 duplicate = asyncio.run(
129 handler.process(
130 {
131 "action": "activate",
132 "context_id": "ctx",
133 "skill": {"name": "demo-skill", "path": "/a0/skills/demo-skill"},
134 },
135 None,
136 )
137 )
138
139 assert duplicate["ok"] is True
140 assert len(context.agent.tool_results) == 1
141
142
143 def test_skills_catalog_deactivate_does_not_remove_loaded_skill(monkeypatch):
144 context = FakeContext()
145 _patch_catalog(monkeypatch, context)
146 context.set_data("loaded_skills", ["demo-skill"])
147 handler = skills_catalog.SkillsCatalog(None, None)
148
149 response = asyncio.run(
150 handler.process(
151 {
152 "action": "deactivate",
153 "context_id": "ctx",
154 "skill": {"name": "demo-skill"},
155 },
156 None,
157 )
158 )
159
160 assert response["ok"] is False
161 assert "cannot be removed" in response["error"]
162 assert context.get_data("loaded_skills") == ["demo-skill"]
163 assert context.agent.tool_results == []