nightshift/tests/test_patches.py
K. Hodges e3679296fd Reliability improvements, integ test fixes
Isolate editor from editing tests for the tutorial, hardcode tests for the integ test, some fixs around isolation. We got the integ working!
2026-05-21 00:31:31 -07:00

299 lines
8.9 KiB
Python

from pathlib import Path
import tempfile
import unittest
from nightshift.config import SafetyConfig
from nightshift.errors import PipelineError
from nightshift.patches import (
generate_patch_from_file_updates,
normalize_patch_text,
parse_file_updates,
repair_hunk_counts,
validate_patch,
)
PATCH = """diff --git a/src/app.py b/src/app.py
--- a/src/app.py
+++ b/src/app.py
@@ -1 +1 @@
-old
+new
"""
class PatchTests(unittest.TestCase):
def test_normalize_extracts_fenced_patch(self) -> None:
text = f"Here it is:\n```diff\n{PATCH}```\n"
self.assertEqual(normalize_patch_text(text), PATCH)
def test_validate_patch_enforces_scopes(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / "src").mkdir()
safety = SafetyConfig(
require_clean_worktree=False,
scoped_paths=("src",),
allowed_commands=(),
forbidden_commands=(),
)
result = validate_patch(PATCH, root, safety)
self.assertEqual(result.files, ("src/app.py",))
self.assertEqual(result.changed_lines, 2)
def test_validate_patch_rejects_forbidden_path(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
safety = SafetyConfig(
require_clean_worktree=False,
scoped_paths=(".",),
allowed_commands=(),
forbidden_commands=(),
)
patch = PATCH.replace("src/app.py", ".nightshift/log.txt")
with self.assertRaisesRegex(PipelineError, "forbidden path"):
validate_patch(patch, root, safety)
def test_validate_patch_enforces_stage_allowed_paths(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / "src").mkdir()
(root / "tests").mkdir()
safety = SafetyConfig(
require_clean_worktree=False,
scoped_paths=("src", "tests"),
allowed_commands=(),
forbidden_commands=(),
)
with self.assertRaisesRegex(PipelineError, "not allowed for this stage"):
validate_patch(PATCH, root, safety, allowed_paths=("tests",))
def test_validate_patch_rejects_malformed_hunk_line(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / "src").mkdir()
safety = SafetyConfig(
require_clean_worktree=False,
scoped_paths=("src",),
allowed_commands=(),
forbidden_commands=(),
)
patch = """diff --git a/src/app.py b/src/app.py
--- a/src/app.py
+++ b/src/app.py
@@ -1 +1,2 @@
-old
+new
bare line
"""
with self.assertRaisesRegex(PipelineError, "malformed hunk line"):
validate_patch(patch, root, safety)
def test_validate_patch_rejects_new_file_when_target_exists(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / "src").mkdir()
(root / "src" / "app.py").write_text("old\n", encoding="utf-8")
safety = SafetyConfig(
require_clean_worktree=False,
scoped_paths=("src",),
allowed_commands=(),
forbidden_commands=(),
)
patch = """diff --git a/src/app.py b/src/app.py
new file mode 100644
--- /dev/null
+++ b/src/app.py
@@ -0,0 +1 @@
+new
"""
with self.assertRaisesRegex(PipelineError, "creates existing file"):
validate_patch(patch, root, safety)
def test_validate_patch_rejects_hunk_count_mismatch(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / "src").mkdir()
safety = SafetyConfig(
require_clean_worktree=False,
scoped_paths=("src",),
allowed_commands=(),
forbidden_commands=(),
)
patch = """diff --git a/src/app.py b/src/app.py
--- a/src/app.py
+++ b/src/app.py
@@ -1 +1,2 @@
-old
+new
"""
with self.assertRaisesRegex(PipelineError, "new line count expected 2, got 1"):
validate_patch(patch, root, safety)
def test_normalize_repairs_hunk_count_mismatch(self) -> None:
lines = "\n".join(f"+line {number}" for number in range(38))
patch = f"""diff --git a/src/app.py b/src/app.py
--- /dev/null
+++ b/src/app.py
@@ -0,0 +1,40 @@
{lines}
"""
normalized = normalize_patch_text(patch)
self.assertIn("@@ -0,0 +1,38 @@", normalized)
def test_validate_patch_counts_hunk_lines_that_look_like_headers(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / "src").mkdir()
safety = SafetyConfig(
require_clean_worktree=False,
scoped_paths=("src",),
allowed_commands=(),
forbidden_commands=(),
)
patch = """diff --git a/src/app.py b/src/app.py
--- a/src/app.py
+++ b/src/app.py
@@ -1,3 +1,3 @@
context
---
----
+++
++++
"""
result = validate_patch(patch, root, safety)
self.assertEqual(result.changed_lines, 4)
def test_repair_hunk_counts_counts_header_like_body_lines(self) -> None:
patch = """diff --git a/src/app.py b/src/app.py
--- a/src/app.py
+++ b/src/app.py
@@ -1 +1 @@
context
---
+++
"""
repaired = repair_hunk_counts(patch)
self.assertIn("@@ -1,2 +1,2 @@", repaired)
def test_validate_patch_accepts_multiple_files(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / "src").mkdir()
safety = SafetyConfig(
require_clean_worktree=False,
scoped_paths=("src",),
allowed_commands=(),
forbidden_commands=(),
)
patch = """diff --git a/src/app.py b/src/app.py
--- a/src/app.py
+++ b/src/app.py
@@ -1 +1 @@
-old
+new
diff --git a/src/test_app.py b/src/test_app.py
--- a/src/test_app.py
+++ b/src/test_app.py
@@ -1 +1 @@
-old test
+new test
"""
result = validate_patch(patch, root, safety)
self.assertEqual(result.files, ("src/app.py", "src/test_app.py"))
def test_file_updates_generate_unified_diff(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / "src").mkdir()
(root / "src" / "app.py").write_text("old\n", encoding="utf-8")
safety = SafetyConfig(
require_clean_worktree=False,
scoped_paths=("src",),
allowed_commands=(),
forbidden_commands=(),
)
updates = parse_file_updates(
"""```file:src/app.py
new
```
```file:src/test_app.py
test
```
"""
)
patch = generate_patch_from_file_updates(updates, root, safety)
result = validate_patch(patch, root, safety)
self.assertIn("diff --git a/src/app.py b/src/app.py", patch)
self.assertIn("diff --git a/src/test_app.py b/src/test_app.py", patch)
self.assertIn("new file mode 100644", patch)
self.assertEqual(result.files, ("src/app.py", "src/test_app.py"))
def test_file_updates_reject_duplicate_blocks(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
safety = SafetyConfig(
require_clean_worktree=False,
scoped_paths=(".",),
allowed_commands=(),
forbidden_commands=(),
)
updates = parse_file_updates(
"""```file:app.py
one
```
```file:app.py
two
```
"""
)
with self.assertRaisesRegex(PipelineError, "duplicate file block"):
generate_patch_from_file_updates(updates, root, safety)
def test_file_updates_allow_identical_duplicate_blocks(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / "app.py").write_text("old\n", encoding="utf-8")
safety = SafetyConfig(
require_clean_worktree=False,
scoped_paths=(".",),
allowed_commands=(),
forbidden_commands=(),
)
updates = parse_file_updates(
"""```file:app.py
new
```
```file:app.py
new
```
"""
)
patch = generate_patch_from_file_updates(updates, root, safety)
self.assertEqual(patch.count("diff --git a/app.py b/app.py"), 1)
if __name__ == "__main__":
unittest.main()