mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-24 21:42:22 +02:00
84 lines
3.5 KiB
Python
84 lines
3.5 KiB
Python
"""The user's know-how travelling up, and their app credentials not travelling at all."""
|
|
|
|
import os
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from runner.run_spec import McpServerNote, RunSpec, SkillPayload
|
|
from runner.seed.data_root import settings_for_run, unavailable_apps_note
|
|
from runner.seed.skills import skills_dir, write_skills
|
|
|
|
COFFEE = {
|
|
"id": "coffee-ratio",
|
|
"files": [
|
|
{"path": "SKILL.md", "text": "---\nname: coffee-ratio\ndescription: house ratio\n---\n\n1:16.5\n"},
|
|
{"path": "scripts/brew.py", "text": "print('brew')\n"},
|
|
],
|
|
}
|
|
|
|
|
|
def p_spec(**overrides) -> RunSpec:
|
|
body = {
|
|
"run_id": "cr-1",
|
|
"workflow": {"id": "wf-1", "title": "Test", "steps": [{"id": "s1", "text": "go"}]},
|
|
"credentials": [{"provider": "anthropic", "auth_type": "api_key", "api_key": "sk-test"}],
|
|
}
|
|
body.update(overrides)
|
|
return RunSpec.model_validate(body)
|
|
|
|
|
|
def test_a_skill_folder_lands_where_the_backend_looks_for_it(tmp_path) -> None:
|
|
written = write_skills(str(tmp_path), [SkillPayload.model_validate(COFFEE)])
|
|
|
|
assert written == 1
|
|
root = skills_dir(str(tmp_path))
|
|
assert root.endswith(os.path.join(".claude", "skills"))
|
|
with open(os.path.join(root, "coffee-ratio", "SKILL.md"), encoding="utf-8") as handle:
|
|
assert "house ratio" in handle.read()
|
|
assert os.path.isfile(os.path.join(root, "coffee-ratio", "scripts", "brew.py"))
|
|
|
|
|
|
def test_no_skills_is_an_empty_directory_not_a_failure(tmp_path) -> None:
|
|
assert write_skills(str(tmp_path), []) == 0
|
|
assert os.path.isdir(skills_dir(str(tmp_path)))
|
|
|
|
|
|
def test_a_skill_with_no_skill_md_is_refused_before_a_machine_boots() -> None:
|
|
with pytest.raises(ValidationError, match="no SKILL.md"):
|
|
SkillPayload.model_validate({"id": "x", "files": [{"path": "README.md", "text": "hi"}]})
|
|
|
|
|
|
@pytest.mark.parametrize("bad", ["../escape.md", "/etc/passwd", "a/../../b.md", "a\\b.md", ""])
|
|
def test_a_skill_path_that_could_climb_out_is_refused(bad: str) -> None:
|
|
with pytest.raises(ValidationError):
|
|
SkillPayload.model_validate({"id": "x", "files": [{"path": bad, "text": "hi"}]})
|
|
|
|
|
|
@pytest.mark.parametrize("bad", ["../evil", "a/b", ".hidden", "has space", ""])
|
|
def test_a_skill_id_that_is_not_a_plain_folder_name_is_refused(bad: str) -> None:
|
|
with pytest.raises(ValidationError):
|
|
SkillPayload.model_validate({"id": bad, "files": [{"path": "SKILL.md", "text": "hi"}]})
|
|
|
|
|
|
def test_the_spec_cannot_carry_an_mcp_secret_at_all() -> None:
|
|
# extra="forbid" is the wall: there is no field for a token, so a payload with one dies here
|
|
# rather than landing in a container that runs the user's own prose with Bash.
|
|
with pytest.raises(ValidationError):
|
|
McpServerNote.model_validate({"name": "Notion", "access_token": "secret_abc"})
|
|
with pytest.raises(ValidationError):
|
|
McpServerNote.model_validate({"name": "Slack", "env": {"SLACK_MCP_XOXC_TOKEN": "xoxc-1"}})
|
|
|
|
|
|
def test_unreachable_apps_are_named_in_the_prompt_so_silence_is_not_mistaken_for_absence(tmp_path) -> None:
|
|
spec = p_spec(unavailable_mcp_servers=[{"name": "Notion"}, {"name": "Google Workspace"}])
|
|
note = unavailable_apps_note(spec)
|
|
|
|
assert "Notion" in note and "Google Workspace" in note
|
|
assert "cannot be done from a cloud run" in note
|
|
assert note in (settings_for_run(spec, str(tmp_path)).default_system_prompt or "")
|
|
|
|
|
|
def test_with_no_connected_apps_nothing_is_added_to_the_prompt(tmp_path) -> None:
|
|
assert unavailable_apps_note(p_spec()) == ""
|