Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,335 @@
|
||||
"""Tests for skill fuzzy patching via tools.fuzzy_match."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import stat
|
||||
|
||||
import pytest
|
||||
|
||||
from tools.skill_manager_tool import (
|
||||
_create_skill,
|
||||
_edit_skill,
|
||||
_patch_skill,
|
||||
_write_file,
|
||||
skill_manage,
|
||||
)
|
||||
|
||||
|
||||
SKILL_CONTENT = """\
|
||||
---
|
||||
name: test-skill
|
||||
description: A test skill for unit testing.
|
||||
---
|
||||
|
||||
# Test Skill
|
||||
|
||||
Step 1: Do the thing.
|
||||
Step 2: Do another thing.
|
||||
Step 3: Final step.
|
||||
"""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fuzzy patching
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestFuzzyPatchSkill:
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_skills(self, tmp_path, monkeypatch):
|
||||
skills_dir = tmp_path / "skills"
|
||||
skills_dir.mkdir()
|
||||
monkeypatch.setattr("tools.skill_manager_tool.SKILLS_DIR", skills_dir)
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
self.skills_dir = skills_dir
|
||||
|
||||
def test_exact_match_still_works(self):
|
||||
_create_skill("test-skill", SKILL_CONTENT)
|
||||
result = _patch_skill("test-skill", "Step 1: Do the thing.", "Step 1: Done!")
|
||||
assert result["success"] is True
|
||||
content = (self.skills_dir / "test-skill" / "SKILL.md").read_text()
|
||||
assert "Step 1: Done!" in content
|
||||
|
||||
def test_whitespace_trimmed_match(self):
|
||||
"""Patch with extra leading whitespace should still find the target."""
|
||||
skill = """\
|
||||
---
|
||||
name: ws-skill
|
||||
description: Whitespace test
|
||||
---
|
||||
|
||||
# Commands
|
||||
|
||||
def hello():
|
||||
print("hi")
|
||||
"""
|
||||
_create_skill("ws-skill", skill)
|
||||
# Agent sends patch with no leading whitespace (common LLM behaviour)
|
||||
result = _patch_skill("ws-skill", "def hello():\n print(\"hi\")", "def hello():\n print(\"hello world\")")
|
||||
assert result["success"] is True
|
||||
content = (self.skills_dir / "ws-skill" / "SKILL.md").read_text()
|
||||
assert 'print("hello world")' in content
|
||||
|
||||
|
||||
def test_multiple_matches_blocked_without_replace_all(self):
|
||||
"""Multiple fuzzy matches should return an error without replace_all."""
|
||||
skill = """\
|
||||
---
|
||||
name: dup-skill
|
||||
description: Duplicate test
|
||||
---
|
||||
|
||||
# Steps
|
||||
|
||||
word word word
|
||||
"""
|
||||
_create_skill("dup-skill", skill)
|
||||
result = _patch_skill("dup-skill", "word", "replaced")
|
||||
assert result["success"] is False
|
||||
assert "match" in result["error"].lower()
|
||||
|
||||
|
||||
def test_skill_manage_patch_uses_fuzzy(self):
|
||||
"""The dispatcher should route to the fuzzy-matching patch."""
|
||||
_create_skill("test-skill", SKILL_CONTENT)
|
||||
raw = skill_manage(
|
||||
action="patch",
|
||||
name="test-skill",
|
||||
old_string=" Step 1: Do the thing.", # extra leading space
|
||||
new_string="Step 1: Updated.",
|
||||
)
|
||||
result = json.loads(raw)
|
||||
# Should succeed via line-trimmed or indentation-flexible matching
|
||||
assert result["success"] is True
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="POSIX permission bits")
|
||||
def test_created_skill_is_group_readable(self):
|
||||
"""New instructional skills use the public-document mode 0644."""
|
||||
_create_skill("mode-skill", SKILL_CONTENT)
|
||||
mode = stat.S_IMODE((self.skills_dir / "mode-skill" / "SKILL.md").stat().st_mode)
|
||||
assert mode == 0o644
|
||||
|
||||
def test_create_rollback_removes_skill_when_scan_blocks(self, monkeypatch):
|
||||
"""Blocked skill creation removes the newly created skill directory."""
|
||||
monkeypatch.setattr(
|
||||
"tools.skill_manager_tool._security_scan_skill",
|
||||
lambda _skill_dir: "blocked",
|
||||
)
|
||||
|
||||
result = _create_skill("blocked-skill", SKILL_CONTENT)
|
||||
|
||||
assert result["success"] is False
|
||||
assert not (self.skills_dir / "blocked-skill").exists()
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="POSIX permission bits")
|
||||
def test_new_documents_are_exactly_0644_under_restrictive_umask(self):
|
||||
"""New skill documents override a restrictive process umask."""
|
||||
old_umask = os.umask(0o077)
|
||||
try:
|
||||
create_result = _create_skill("umask-skill", SKILL_CONTENT)
|
||||
write_result = _write_file(
|
||||
"umask-skill", "references/example.md", "# Reference\n"
|
||||
)
|
||||
finally:
|
||||
os.umask(old_umask)
|
||||
|
||||
assert create_result["success"] is True
|
||||
assert write_result["success"] is True
|
||||
skill_md = self.skills_dir / "umask-skill" / "SKILL.md"
|
||||
reference = self.skills_dir / "umask-skill" / "references/example.md"
|
||||
assert stat.S_IMODE(skill_md.stat().st_mode) == 0o644
|
||||
assert stat.S_IMODE(reference.stat().st_mode) == 0o644
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="POSIX permission bits")
|
||||
def test_explicit_skill_md_patch_preserves_existing_mode(self):
|
||||
"""Explicit SKILL.md patch paths preserve the existing document mode."""
|
||||
_create_skill("explicit-skill", SKILL_CONTENT)
|
||||
skill_md = self.skills_dir / "explicit-skill" / "SKILL.md"
|
||||
skill_md.chmod(0o660)
|
||||
|
||||
result = _patch_skill(
|
||||
"explicit-skill",
|
||||
"Step 1: Do the thing.",
|
||||
"Step 1: Done!",
|
||||
file_path="SKILL.md",
|
||||
)
|
||||
|
||||
assert result["success"] is True
|
||||
assert "Step 1: Done!" in skill_md.read_text(encoding="utf-8")
|
||||
assert stat.S_IMODE(skill_md.stat().st_mode) == 0o660
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="POSIX permission bits")
|
||||
@pytest.mark.parametrize("mode", [0o600, 0o660])
|
||||
def test_edit_preserves_existing_mode(self, mode):
|
||||
"""Full skill edits must preserve private and shared document modes."""
|
||||
_create_skill("mode-skill", SKILL_CONTENT)
|
||||
skill_md = self.skills_dir / "mode-skill" / "SKILL.md"
|
||||
skill_md.chmod(mode)
|
||||
replacement = SKILL_CONTENT.replace("Step 1: Do the thing.", "Step 1: Done!")
|
||||
|
||||
result = _edit_skill("mode-skill", replacement)
|
||||
|
||||
assert result["success"] is True
|
||||
assert stat.S_IMODE(skill_md.stat().st_mode) == mode
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="POSIX permission bits")
|
||||
@pytest.mark.parametrize("mode", [0o600, 0o660])
|
||||
def test_patched_skill_preserves_existing_mode(self, mode):
|
||||
"""Atomic patching must preserve both private and shared modes."""
|
||||
_create_skill("mode-skill", SKILL_CONTENT)
|
||||
skill_md = self.skills_dir / "mode-skill" / "SKILL.md"
|
||||
skill_md.chmod(mode)
|
||||
|
||||
result = _patch_skill("mode-skill", "Step 1: Do the thing.", "Step 1: Done!")
|
||||
|
||||
assert result["success"] is True
|
||||
assert stat.S_IMODE(skill_md.stat().st_mode) == mode
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="POSIX permission bits")
|
||||
def test_supporting_file_write_uses_group_readable_mode(self):
|
||||
"""New reference files should follow the same document mode."""
|
||||
_create_skill("mode-skill", SKILL_CONTENT)
|
||||
|
||||
result = _write_file(
|
||||
"mode-skill",
|
||||
"references/example.md",
|
||||
"# Reference\n",
|
||||
)
|
||||
|
||||
assert result["success"] is True
|
||||
reference = self.skills_dir / "mode-skill" / "references/example.md"
|
||||
assert stat.S_IMODE(reference.stat().st_mode) == 0o644
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="POSIX permission bits")
|
||||
@pytest.mark.parametrize("mode", [0o600, 0o660])
|
||||
def test_supporting_file_write_preserves_existing_mode(self, mode):
|
||||
"""Overwriting a reference preserves its existing private or shared mode."""
|
||||
_create_skill("mode-skill", SKILL_CONTENT)
|
||||
reference = self.skills_dir / "mode-skill" / "references/example.md"
|
||||
reference.parent.mkdir()
|
||||
reference.write_text("old\n", encoding="utf-8")
|
||||
reference.chmod(mode)
|
||||
|
||||
result = _write_file("mode-skill", "references/example.md", "new\n")
|
||||
|
||||
assert result["success"] is True
|
||||
assert stat.S_IMODE(reference.stat().st_mode) == mode
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="POSIX permission bits")
|
||||
@pytest.mark.parametrize("mode", [0o600, 0o660])
|
||||
def test_supporting_file_patch_preserves_existing_mode(self, mode):
|
||||
"""Patching a reference preserves its existing private or shared mode."""
|
||||
_create_skill("mode-skill", SKILL_CONTENT)
|
||||
reference = self.skills_dir / "mode-skill" / "references/example.md"
|
||||
reference.parent.mkdir()
|
||||
reference.write_text("old\n", encoding="utf-8")
|
||||
reference.chmod(mode)
|
||||
|
||||
result = _patch_skill(
|
||||
"mode-skill",
|
||||
"old",
|
||||
"new",
|
||||
file_path="references/example.md",
|
||||
)
|
||||
|
||||
assert result["success"] is True
|
||||
assert reference.read_text(encoding="utf-8") == "new\n"
|
||||
assert stat.S_IMODE(reference.stat().st_mode) == mode
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="POSIX permission bits")
|
||||
def test_supporting_file_patch_rollback_preserves_mode_when_scan_blocks(
|
||||
self, monkeypatch
|
||||
):
|
||||
"""Blocked reference patches restore content and the original mode."""
|
||||
_create_skill("rollback-skill", SKILL_CONTENT)
|
||||
reference = self.skills_dir / "rollback-skill" / "references/example.md"
|
||||
reference.parent.mkdir()
|
||||
reference.write_text("original\n", encoding="utf-8")
|
||||
reference.chmod(0o660)
|
||||
monkeypatch.setattr(
|
||||
"tools.skill_manager_tool._security_scan_skill",
|
||||
lambda _skill_dir: "blocked",
|
||||
)
|
||||
|
||||
result = _patch_skill(
|
||||
"rollback-skill",
|
||||
"original",
|
||||
"blocked",
|
||||
file_path="references/example.md",
|
||||
)
|
||||
|
||||
assert result["success"] is False
|
||||
assert reference.read_text(encoding="utf-8") == "original\n"
|
||||
assert stat.S_IMODE(reference.stat().st_mode) == 0o660
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="POSIX permission bits")
|
||||
def test_edit_rollback_preserves_existing_mode_when_scan_blocks(self, monkeypatch):
|
||||
"""Blocked full edits restore both content and the original mode."""
|
||||
_create_skill("rollback-skill", SKILL_CONTENT)
|
||||
skill_md = self.skills_dir / "rollback-skill" / "SKILL.md"
|
||||
skill_md.chmod(0o660)
|
||||
replacement = SKILL_CONTENT.replace("Step 1: Do the thing.", "blocked edit")
|
||||
monkeypatch.setattr(
|
||||
"tools.skill_manager_tool._security_scan_skill",
|
||||
lambda _skill_dir: "blocked",
|
||||
)
|
||||
|
||||
result = _edit_skill("rollback-skill", replacement)
|
||||
|
||||
assert result["success"] is False
|
||||
assert skill_md.read_text(encoding="utf-8") == SKILL_CONTENT
|
||||
assert stat.S_IMODE(skill_md.stat().st_mode) == 0o660
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="POSIX permission bits")
|
||||
def test_patch_rollback_preserves_existing_mode_when_scan_blocks(self, monkeypatch):
|
||||
"""Blocked patches restore both content and the original mode."""
|
||||
_create_skill("rollback-skill", SKILL_CONTENT)
|
||||
skill_md = self.skills_dir / "rollback-skill" / "SKILL.md"
|
||||
skill_md.chmod(0o600)
|
||||
monkeypatch.setattr(
|
||||
"tools.skill_manager_tool._security_scan_skill",
|
||||
lambda _skill_dir: "blocked",
|
||||
)
|
||||
|
||||
result = _patch_skill(
|
||||
"rollback-skill", "Step 1: Do the thing.", "blocked patch"
|
||||
)
|
||||
|
||||
assert result["success"] is False
|
||||
assert skill_md.read_text(encoding="utf-8") == SKILL_CONTENT
|
||||
assert stat.S_IMODE(skill_md.stat().st_mode) == 0o600
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="POSIX permission bits")
|
||||
def test_supporting_file_rollback_preserves_existing_mode_when_scan_blocks(
|
||||
self, monkeypatch
|
||||
):
|
||||
"""Blocked supporting-file overwrites restore content and mode."""
|
||||
_create_skill("rollback-skill", SKILL_CONTENT)
|
||||
reference = self.skills_dir / "rollback-skill" / "references/example.md"
|
||||
reference.parent.mkdir()
|
||||
reference.write_text("original\n", encoding="utf-8")
|
||||
reference.chmod(0o660)
|
||||
monkeypatch.setattr(
|
||||
"tools.skill_manager_tool._security_scan_skill",
|
||||
lambda _skill_dir: "blocked",
|
||||
)
|
||||
|
||||
result = _write_file("rollback-skill", "references/example.md", "blocked\n")
|
||||
|
||||
assert result["success"] is False
|
||||
assert reference.read_text(encoding="utf-8") == "original\n"
|
||||
assert stat.S_IMODE(reference.stat().st_mode) == 0o660
|
||||
|
||||
def test_new_supporting_file_rollback_removes_file_when_scan_blocks(self, monkeypatch):
|
||||
"""Blocked supporting-file creates remove the newly written file."""
|
||||
_create_skill("rollback-skill", SKILL_CONTENT)
|
||||
reference = self.skills_dir / "rollback-skill" / "references/example.md"
|
||||
monkeypatch.setattr(
|
||||
"tools.skill_manager_tool._security_scan_skill",
|
||||
lambda _skill_dir: "blocked",
|
||||
)
|
||||
|
||||
result = _write_file("rollback-skill", "references/example.md", "blocked\n")
|
||||
|
||||
assert result["success"] is False
|
||||
assert not reference.exists()
|
||||
Reference in New Issue
Block a user