-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathtest_skills_e2e.py
More file actions
236 lines (189 loc) · 8.82 KB
/
test_skills_e2e.py
File metadata and controls
236 lines (189 loc) · 8.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
"""
Tests for skills configuration functionality
"""
import os
import shutil
import pytest
from copilot.generated.rpc import SkillSource
from copilot.session import CustomAgentConfig, PermissionHandler
from .testharness import E2ETestContext
pytestmark = pytest.mark.asyncio(loop_scope="module")
SKILL_MARKER = "PINEAPPLE_COCONUT_42"
@pytest.fixture(autouse=True)
def clean_skills_dir(ctx: E2ETestContext):
"""Ensure we start fresh each time"""
skills_dir = os.path.join(ctx.work_dir, ".test_skills")
if os.path.exists(skills_dir):
shutil.rmtree(skills_dir)
yield
def create_skill_dir(work_dir: str) -> str:
"""Create a skills directory in the working directory"""
skills_dir = os.path.join(work_dir, ".test_skills")
os.makedirs(skills_dir, exist_ok=True)
# Create a skill subdirectory with SKILL.md
skill_subdir = os.path.join(skills_dir, "test-skill")
os.makedirs(skill_subdir, exist_ok=True)
# Create a skill that instructs the model to include a specific marker in responses
skill_content = f"""---
name: test-skill
description: A test skill that adds a marker to responses
---
# Test Skill Instructions
IMPORTANT: You MUST include the exact text "{SKILL_MARKER}" somewhere in EVERY response you give. \
This is a mandatory requirement. Include it naturally in your response.
""".replace("\r", "")
with open(os.path.join(skill_subdir, "SKILL.md"), "w", newline="\n") as f:
f.write(skill_content)
return skills_dir
class TestSkillBehavior:
async def test_should_load_and_apply_skill_from_skilldirectories(self, ctx: E2ETestContext):
"""Test that skills are loaded and applied from skillDirectories"""
skills_dir = create_skill_dir(ctx.work_dir)
session = await ctx.client.create_session(
on_permission_request=PermissionHandler.approve_all, skill_directories=[skills_dir]
)
assert session.session_id is not None
# The skill instructs the model to include a marker - verify it appears
message = await session.send_and_wait("Say hello briefly using the test skill.")
assert message is not None
assert SKILL_MARKER in message.data.content
await session.disconnect()
async def test_should_not_apply_skill_when_disabled_via_disabledskills(
self, ctx: E2ETestContext
):
"""Test that disabledSkills prevents skill from being applied"""
skills_dir = create_skill_dir(ctx.work_dir)
session = await ctx.client.create_session(
on_permission_request=PermissionHandler.approve_all,
skill_directories=[skills_dir],
disabled_skills=["test-skill"],
)
assert session.session_id is not None
# The skill is disabled, so the marker should NOT appear
message = await session.send_and_wait("Say hello briefly using the test skill.")
assert message is not None
assert SKILL_MARKER not in message.data.content
await session.disconnect()
async def test_should_allow_agent_with_skills_to_invoke_skill(self, ctx: E2ETestContext):
"""Test that an agent with skills gets skill content preloaded into context"""
skills_dir = create_skill_dir(ctx.work_dir)
custom_agents: list[CustomAgentConfig] = [
{
"name": "skill-agent",
"description": "An agent with access to test-skill",
"prompt": "You are a helpful test agent.",
"skills": ["test-skill"],
}
]
session = await ctx.client.create_session(
on_permission_request=PermissionHandler.approve_all,
skill_directories=[skills_dir],
custom_agents=custom_agents,
agent="skill-agent",
)
assert session.session_id is not None
# The agent has skills: ["test-skill"], so the skill content is preloaded into its context
message = await session.send_and_wait("Say hello briefly using the test skill.")
assert message is not None
assert SKILL_MARKER in message.data.content
await session.disconnect()
async def test_should_not_provide_skills_to_agent_without_skills_field(
self, ctx: E2ETestContext
):
"""Test that an agent without skills field gets no skill content (opt-in model)"""
skills_dir = create_skill_dir(ctx.work_dir)
custom_agents: list[CustomAgentConfig] = [
{
"name": "no-skill-agent",
"description": "An agent without skills access",
"prompt": "You are a helpful test agent.",
}
]
session = await ctx.client.create_session(
on_permission_request=PermissionHandler.approve_all,
skill_directories=[skills_dir],
custom_agents=custom_agents,
agent="no-skill-agent",
)
assert session.session_id is not None
# The agent has no skills field, so no skill content is injected
message = await session.send_and_wait("Say hello briefly using the test skill.")
assert message is not None
assert SKILL_MARKER not in message.data.content
await session.disconnect()
@pytest.mark.skip(
reason="See the big comment around the equivalent test in the Node SDK. "
"Skipped because the feature doesn't work correctly yet."
)
async def test_should_apply_skill_on_session_resume_with_skilldirectories(
self, ctx: E2ETestContext
):
"""Test that skills are applied when added on session resume"""
skills_dir = create_skill_dir(ctx.work_dir)
# Create a session without skills first
session1 = await ctx.client.create_session(
on_permission_request=PermissionHandler.approve_all
)
session_id = session1.session_id
# First message without skill - marker should not appear
message1 = await session1.send_and_wait("Say hi.")
assert message1 is not None
assert SKILL_MARKER not in message1.data.content
# Resume with skillDirectories - skill should now be active
session2 = await ctx.client.resume_session(
session_id,
on_permission_request=PermissionHandler.approve_all,
skill_directories=[skills_dir],
)
assert session2.session_id == session_id
# Now the skill should be applied
message2 = await session2.send_and_wait("Say hello again using the test skill.")
assert message2 is not None
assert SKILL_MARKER in message2.data.content
await session2.disconnect()
async def test_should_control_ambient_project_skills_with_enableconfigdiscovery(
self, ctx: E2ETestContext
):
"""Test that EnableConfigDiscovery toggles discovery of project-level skills.
Project-level skills live under ``.github/skills`` in the working directory.
"""
import uuid
project_dir = os.path.join(ctx.work_dir, f"config-discovery-{uuid.uuid4().hex}")
project_skills_dir = os.path.join(project_dir, ".github", "skills")
skill_name = f"ambient-skill-{uuid.uuid4().hex}"[:32]
os.makedirs(project_skills_dir, exist_ok=True)
skill_subdir = os.path.join(project_skills_dir, skill_name)
os.makedirs(skill_subdir, exist_ok=True)
skill_content = (
"---\n"
f"name: {skill_name}\n"
"description: A project skill discovered from .github/skills\n"
"---\n"
"\n"
"Use the exact phrase AMBIENT_DISCOVERY_SKILL when this skill is active.\n"
)
with open(os.path.join(skill_subdir, "SKILL.md"), "w", newline="\n") as f:
f.write(skill_content)
# Disabled discovery: project skills should be hidden.
disabled_session = await ctx.client.create_session(
on_permission_request=PermissionHandler.approve_all,
working_directory=project_dir,
enable_config_discovery=False,
)
disabled_skills = await disabled_session.rpc.skills.list()
assert not any(s.name == skill_name for s in disabled_skills.skills)
await disabled_session.disconnect()
# Enabled discovery: project skills should be present and active.
enabled_session = await ctx.client.create_session(
on_permission_request=PermissionHandler.approve_all,
working_directory=project_dir,
enable_config_discovery=True,
)
enabled_skills = await enabled_session.rpc.skills.list()
discovered = [s for s in enabled_skills.skills if s.name == skill_name]
assert len(discovered) == 1
skill = discovered[0]
assert skill.enabled is True
assert skill.source == SkillSource.PROJECT
assert skill.path.endswith(os.path.join(skill_name, "SKILL.md"))
await enabled_session.disconnect()