mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-30 10:52:15 +00:00
- Rewrite .github/workflows/desktop.yml: 2-job pipeline (validate + build-and-release) with rolling desktop-latest pre-release on push to main and stable desktop-v* releases - Add UpdateChecker component: checks for updates on startup + every 30 min, background download with progress bar, one-click relaunch - Configure Tauri updater: endpoints pointing to desktop-latest release, pubkey placeholder - Add tauri-plugin-process for relaunch support (Cargo.toml, lib.rs, package.json) - Add macOS Entitlements.plist for notarization (network + file access, no sandbox) - Add scripts/bump-desktop-version.sh for atomic version bumps across 3 config files - Add desktop/README.md with dev setup, auto-update architecture, signing docs - Update .gitignore for desktop/node_modules, dist, target - Configure macOS minimumSystemVersion, Windows timestampUrl - Include all Phase 14-21 work: agent hardening, RBAC, taint tracking, workflows, skills, knowledge graph, sessions, A2A, MCP templates, WASM sandbox, TUI dashboard, production tools, CLI expansion, API expansion, learning productionization, Tauri desktop app, and 10 new channels Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
213 lines
7.0 KiB
Python
213 lines
7.0 KiB
Python
"""Tests for the apply_patch tool."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from openjarvis.tools.apply_patch import ApplyPatchTool
|
|
|
|
|
|
class TestApplyPatchTool:
|
|
def test_spec(self):
|
|
tool = ApplyPatchTool()
|
|
assert tool.spec.name == "apply_patch"
|
|
assert tool.spec.category == "filesystem"
|
|
assert "file:write" in tool.spec.required_capabilities
|
|
|
|
def test_no_patch_provided(self):
|
|
tool = ApplyPatchTool()
|
|
result = tool.execute(patch="")
|
|
assert result.success is False
|
|
assert "No patch provided" in result.content
|
|
|
|
def test_simple_one_hunk_patch(self, tmp_path):
|
|
f = tmp_path / "hello.txt"
|
|
f.write_text("line1\nline2\nline3\n", encoding="utf-8")
|
|
patch = (
|
|
"--- a/hello.txt\n"
|
|
"+++ b/hello.txt\n"
|
|
"@@ -1,3 +1,3 @@\n"
|
|
" line1\n"
|
|
"-line2\n"
|
|
"+line2_modified\n"
|
|
" line3\n"
|
|
)
|
|
tool = ApplyPatchTool()
|
|
result = tool.execute(patch=patch, path=str(f), backup=False)
|
|
assert result.success is True
|
|
assert result.metadata["hunks_applied"] == 1
|
|
content = f.read_text(encoding="utf-8")
|
|
assert "line2_modified" in content
|
|
assert "line2\n" not in content
|
|
|
|
def test_multi_hunk_patch(self, tmp_path):
|
|
f = tmp_path / "multi.txt"
|
|
f.write_text(
|
|
"alpha\nbeta\ngamma\ndelta\nepsilon\nzeta\neta\ntheta\n",
|
|
encoding="utf-8",
|
|
)
|
|
patch = (
|
|
"--- a/multi.txt\n"
|
|
"+++ b/multi.txt\n"
|
|
"@@ -1,3 +1,3 @@\n"
|
|
" alpha\n"
|
|
"-beta\n"
|
|
"+BETA\n"
|
|
" gamma\n"
|
|
"@@ -6,3 +6,3 @@\n"
|
|
" zeta\n"
|
|
"-eta\n"
|
|
"+ETA\n"
|
|
" theta\n"
|
|
)
|
|
tool = ApplyPatchTool()
|
|
result = tool.execute(patch=patch, path=str(f), backup=False)
|
|
assert result.success is True
|
|
assert result.metadata["hunks_applied"] == 2
|
|
content = f.read_text(encoding="utf-8")
|
|
assert "BETA" in content
|
|
assert "ETA" in content
|
|
assert "beta" not in content
|
|
lines = content.splitlines()
|
|
assert "eta" not in lines
|
|
|
|
def test_context_mismatch_error(self, tmp_path):
|
|
f = tmp_path / "mismatch.txt"
|
|
f.write_text("aaa\nbbb\nccc\n", encoding="utf-8")
|
|
patch = (
|
|
"--- a/mismatch.txt\n"
|
|
"+++ b/mismatch.txt\n"
|
|
"@@ -1,3 +1,3 @@\n"
|
|
" aaa\n"
|
|
"-WRONG_CONTENT\n"
|
|
"+replaced\n"
|
|
" ccc\n"
|
|
)
|
|
tool = ApplyPatchTool()
|
|
result = tool.execute(patch=patch, path=str(f), backup=False)
|
|
assert result.success is False
|
|
assert "mismatch" in result.content.lower()
|
|
|
|
def test_backup_creation(self, tmp_path):
|
|
f = tmp_path / "backup_me.txt"
|
|
f.write_text("original\ncontent\n", encoding="utf-8")
|
|
patch = (
|
|
"--- a/backup_me.txt\n"
|
|
"+++ b/backup_me.txt\n"
|
|
"@@ -1,2 +1,2 @@\n"
|
|
" original\n"
|
|
"-content\n"
|
|
"+new_content\n"
|
|
)
|
|
tool = ApplyPatchTool()
|
|
result = tool.execute(patch=patch, path=str(f), backup=True)
|
|
assert result.success is True
|
|
assert "backup_path" in result.metadata
|
|
bak = tmp_path / "backup_me.txt.bak"
|
|
assert bak.exists()
|
|
assert bak.read_text(encoding="utf-8") == "original\ncontent\n"
|
|
assert "new_content" in f.read_text(encoding="utf-8")
|
|
|
|
def test_backup_disabled(self, tmp_path):
|
|
f = tmp_path / "no_bak.txt"
|
|
f.write_text("hello\nworld\n", encoding="utf-8")
|
|
patch = (
|
|
"--- a/no_bak.txt\n"
|
|
"+++ b/no_bak.txt\n"
|
|
"@@ -1,2 +1,2 @@\n"
|
|
"-hello\n"
|
|
"+goodbye\n"
|
|
" world\n"
|
|
)
|
|
tool = ApplyPatchTool()
|
|
result = tool.execute(patch=patch, path=str(f), backup=False)
|
|
assert result.success is True
|
|
assert "backup_path" not in result.metadata
|
|
bak = tmp_path / "no_bak.txt.bak"
|
|
assert not bak.exists()
|
|
|
|
def test_blocks_sensitive_files(self, tmp_path):
|
|
f = tmp_path / ".env"
|
|
f.write_text("SECRET=foo\n", encoding="utf-8")
|
|
patch = (
|
|
"--- a/.env\n"
|
|
"+++ b/.env\n"
|
|
"@@ -1 +1 @@\n"
|
|
"-SECRET=foo\n"
|
|
"+SECRET=bar\n"
|
|
)
|
|
tool = ApplyPatchTool()
|
|
result = tool.execute(patch=patch, path=str(f))
|
|
assert result.success is False
|
|
assert "sensitive" in result.content.lower()
|
|
|
|
def test_auto_detect_path_from_patch_header(self, tmp_path):
|
|
f = tmp_path / "auto.txt"
|
|
f.write_text("one\ntwo\nthree\n", encoding="utf-8")
|
|
patch = (
|
|
"--- a/auto.txt\n"
|
|
f"+++ b/{f}\n"
|
|
"@@ -1,3 +1,3 @@\n"
|
|
" one\n"
|
|
"-two\n"
|
|
"+TWO\n"
|
|
" three\n"
|
|
)
|
|
tool = ApplyPatchTool()
|
|
# No explicit path — should auto-detect from +++ header
|
|
result = tool.execute(patch=patch, backup=False)
|
|
assert result.success is True
|
|
assert "TWO" in f.read_text(encoding="utf-8")
|
|
|
|
def test_malformed_patch(self):
|
|
tool = ApplyPatchTool()
|
|
result = tool.execute(patch="this is not a patch at all")
|
|
assert result.success is False
|
|
lower = result.content.lower()
|
|
assert "malformed" in lower or "no hunks" in lower
|
|
|
|
def test_file_not_found(self):
|
|
tool = ApplyPatchTool()
|
|
patch = (
|
|
"--- a/nonexistent.txt\n"
|
|
"+++ b/nonexistent.txt\n"
|
|
"@@ -1 +1 @@\n"
|
|
"-old\n"
|
|
"+new\n"
|
|
)
|
|
result = tool.execute(patch=patch, path="/nonexistent/path/file.txt")
|
|
assert result.success is False
|
|
assert "not found" in result.content.lower()
|
|
|
|
def test_addition_only_hunk(self, tmp_path):
|
|
f = tmp_path / "add_only.txt"
|
|
f.write_text("first\nsecond\n", encoding="utf-8")
|
|
patch = (
|
|
"--- a/add_only.txt\n"
|
|
"+++ b/add_only.txt\n"
|
|
"@@ -1,2 +1,3 @@\n"
|
|
" first\n"
|
|
"+inserted\n"
|
|
" second\n"
|
|
)
|
|
tool = ApplyPatchTool()
|
|
result = tool.execute(patch=patch, path=str(f), backup=False)
|
|
assert result.success is True
|
|
content = f.read_text(encoding="utf-8")
|
|
assert content == "first\ninserted\nsecond\n"
|
|
|
|
def test_removal_only_hunk(self, tmp_path):
|
|
f = tmp_path / "del_only.txt"
|
|
f.write_text("keep\nremove_me\nkeep_too\n", encoding="utf-8")
|
|
patch = (
|
|
"--- a/del_only.txt\n"
|
|
"+++ b/del_only.txt\n"
|
|
"@@ -1,3 +1,2 @@\n"
|
|
" keep\n"
|
|
"-remove_me\n"
|
|
" keep_too\n"
|
|
)
|
|
tool = ApplyPatchTool()
|
|
result = tool.execute(patch=patch, path=str(f), backup=False)
|
|
assert result.success is True
|
|
content = f.read_text(encoding="utf-8")
|
|
assert content == "keep\nkeep_too\n"
|