@@ -153,15 +153,18 @@ def test_fork_skill_persistence_dir_forwarded(persistence_dir):
153153
154154
155155@pytest .mark .parametrize (
156- "persistence_dir, expected" ,
156+ "persistence_dir, skill_name, expected" ,
157157 [
158- (None , None ),
159- ("/state/abc123" , "/state/abc123/forks/ddebug" ),
158+ (None , "ddebug" , None ),
159+ ("/state/abc123" , "ddebug" , "/state/abc123/forks/ddebug" ),
160+ # Path-unsafe characters are sanitized to avoid nested dirs or traversal
161+ ("/state/abc123" , "subdir/my_skill" , "/state/abc123/forks/subdir_my_skill" ),
162+ ("/state/abc123" , "../evil" , "/state/abc123/forks/___evil" ),
160163 ],
161164)
162- def test_fork_persistence_dir_path_construction (persistence_dir , expected ):
163- """builds <persistence_dir>/forks/<skill.name > before passing to Conversation."""
164- skill = _fork_skill ()
165+ def test_fork_persistence_dir_path_construction (persistence_dir , skill_name , expected ):
166+ """builds <persistence_dir>/forks/<safe_name > before passing to Conversation."""
167+ skill = _fork_skill (name = skill_name )
165168 mock_agent = MagicMock ()
166169 mock_agent .agent_context = None
167170
@@ -204,3 +207,37 @@ def test_fork_skill_fallback_when_agent_or_working_dir_missing(
204207 assert result is not None
205208 content , _ = result
206209 assert "inline fallback content" in content .text
210+
211+
212+ def test_subagent_context_keeps_inline_skills_drops_forks ():
213+ """Forked subagent retains inline skills but not other fork skills,
214+ and preserves system_message_suffix."""
215+ fork_skill = _fork_skill (name = "ddebug" )
216+ other_fork = _fork_skill (name = "other_fork" )
217+ inline_skill = Skill (
218+ name = "inline_helper" ,
219+ content = "inline body" ,
220+ trigger = KeywordTrigger (keywords = ["helper" ]),
221+ context = "inline" ,
222+ )
223+ parent_ctx = AgentContext (
224+ skills = [fork_skill , other_fork , inline_skill ],
225+ system_message_suffix = "preserve me" ,
226+ )
227+ mock_agent = MagicMock ()
228+ mock_agent .agent_context = parent_ctx
229+ # model_copy on the agent itself: capture what gets passed
230+ mock_agent .model_copy .return_value = MagicMock ()
231+
232+ with patch (
233+ "openhands.sdk.conversation.conversation.Conversation"
234+ ) as MockConversation :
235+ mock_conv = MagicMock ()
236+ mock_conv .state .events = []
237+ MockConversation .return_value = mock_conv
238+ run_skill_forked (fork_skill , mock_agent , "/workspace" )
239+
240+ _ , agent_copy_kwargs = mock_agent .model_copy .call_args
241+ sub_ctx = agent_copy_kwargs ["update" ]["agent_context" ]
242+ assert [s .name for s in sub_ctx .skills ] == ["inline_helper" ]
243+ assert sub_ctx .system_message_suffix == "preserve me"
0 commit comments