mirror of
https://github.com/khodges42/nightShift.git
synced 2026-06-14 10:08:37 +00:00
Isolate editor from editing tests for the tutorial, hardcode tests for the integ test, some fixs around isolation. We got the integ working!
299 lines
8.9 KiB
Python
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()
|