cluxion-agentplugin-supercoder 0.2.5__tar.gz → 0.2.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/PKG-INFO +1 -1
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/pyproject.toml +1 -1
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/__init__.py +1 -1
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/core/hash_patch.py +78 -23
- cluxion_agentplugin_supercoder-0.2.7/tests/test_hash_patch.py +201 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/uv.lock +1 -1
- cluxion_agentplugin_supercoder-0.2.5/tests/test_hash_patch.py +0 -82
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/.github/workflows/ci.yml +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/.github/workflows/publish.yml +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/.gitignore +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/ARCHITECTURE.md +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/Docs/README.md +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/Docs/agent-surfaces.md +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/Docs/architecture.md +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/Docs/capabilities.md +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/Docs/design.md +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/Docs/installation.md +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/Docs/rust-architecture.md +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/Docs/tools.md +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/LICENSE +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/README.md +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/__init__.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/adapters/claude/.claude-plugin/plugin.json +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/adapters/claude/skills/supercoder/SKILL.md +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/adapters/codex/config-snippet.toml +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/adapters/hermes/README.md +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/plugin.yaml +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/rust/supercoder_index/Cargo.lock +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/rust/supercoder_index/Cargo.toml +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/rust/supercoder_index/pyproject.toml +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/rust/supercoder_index/src/lib.rs +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/rust/supercoder_index/src/main.rs +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/rust/supercoder_index/src/outline.rs +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/rust/supercoder_index/src/syntax.rs +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/scripts/repack_native_wheel.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/cli.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/core/cursor.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/core/line_budget.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/core/lint_gate.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/core/queue.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/core/repo_map.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/core/retry_loop.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/core/safety.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/core/syntax_gate.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/core/test_gate.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/doctor/__init__.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/doctor/catalog.json +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/doctor/framework.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/doctor/probes.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/plugin.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/runner.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/rust_bridge.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/schemas.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/tests/test_cursor.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/tests/test_doctor.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/tests/test_line_budget.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/tests/test_lint_gate.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/tests/test_plugin.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/tests/test_queue.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/tests/test_repo_map.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/tests/test_retry_loop.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/tests/test_rust_bridge.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/tests/test_safety.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/tests/test_syntax_gate.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/tests/test_test_gate.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cluxion-agentplugin-supercoder
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.7
|
|
4
4
|
Summary: Universal agent coding harness plugin: cursor logic, safe patch, line budget, Rust index, test gates.
|
|
5
5
|
Project-URL: Homepage, https://github.com/cluxion/cluxion-Agentplugin-supercoder
|
|
6
6
|
Project-URL: Repository, https://github.com/cluxion/cluxion-Agentplugin-supercoder
|
{cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/pyproject.toml
RENAMED
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cluxion-agentplugin-supercoder"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.7"
|
|
8
8
|
description = "Universal agent coding harness plugin: cursor logic, safe patch, line budget, Rust index, test gates."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -3,15 +3,40 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import hashlib
|
|
6
|
+
import os
|
|
7
|
+
import tempfile
|
|
8
|
+
from contextlib import contextmanager
|
|
6
9
|
from dataclasses import dataclass
|
|
7
10
|
from difflib import SequenceMatcher
|
|
8
11
|
from pathlib import Path
|
|
9
12
|
|
|
13
|
+
try:
|
|
14
|
+
import fcntl
|
|
15
|
+
except ImportError:
|
|
16
|
+
fcntl = None # type: ignore[assignment]
|
|
17
|
+
|
|
10
18
|
DEFAULT_FUZZY_THRESHOLD = 0.86
|
|
11
19
|
MAX_CONTEXT_SCAN = 8
|
|
12
20
|
MAX_LINE_DRIFT = 2
|
|
13
21
|
|
|
14
22
|
|
|
23
|
+
@contextmanager
|
|
24
|
+
def _exclusive_lock(path: Path):
|
|
25
|
+
"""Exclusive advisory lock on target file (fcntl.flock). Graceful degrade on non-POSIX."""
|
|
26
|
+
if fcntl is None or not path.exists():
|
|
27
|
+
yield
|
|
28
|
+
return
|
|
29
|
+
fd = os.open(str(path), os.O_RDWR)
|
|
30
|
+
try:
|
|
31
|
+
fcntl.flock(fd, fcntl.LOCK_EX)
|
|
32
|
+
yield
|
|
33
|
+
finally:
|
|
34
|
+
try:
|
|
35
|
+
fcntl.flock(fd, fcntl.LOCK_UN)
|
|
36
|
+
finally:
|
|
37
|
+
os.close(fd)
|
|
38
|
+
|
|
39
|
+
|
|
15
40
|
@dataclass(frozen=True, slots=True)
|
|
16
41
|
class PatchResult:
|
|
17
42
|
success: bool
|
|
@@ -44,28 +69,29 @@ def apply_patch(
|
|
|
44
69
|
) -> PatchResult:
|
|
45
70
|
if not path.exists():
|
|
46
71
|
return _failed(str(path), "missing_file", expected_file_hash, "file not found")
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
72
|
+
with _exclusive_lock(path):
|
|
73
|
+
text = path.read_text(encoding="utf-8")
|
|
74
|
+
current_hash = file_hash(text)
|
|
75
|
+
if expected_file_hash and current_hash != _normalize_hash(expected_file_hash):
|
|
76
|
+
return _failed(str(path), "stale_file", expected_file_hash, "file changed since cursor was created")
|
|
77
|
+
exact = _exact_spans(text, old_text)
|
|
78
|
+
if exact:
|
|
79
|
+
start, end = exact[0]
|
|
80
|
+
return _commit(path, text, start, end, new_text, "exact", expected_file_hash or current_hash, current_hash, 1.0)
|
|
81
|
+
fuzzy = _best_fuzzy_span(text, old_text)
|
|
82
|
+
if fuzzy and fuzzy[3] >= fuzzy_threshold and not fuzzy[4]:
|
|
83
|
+
return _commit(
|
|
84
|
+
path,
|
|
85
|
+
text,
|
|
86
|
+
fuzzy[0],
|
|
87
|
+
fuzzy[1],
|
|
88
|
+
new_text,
|
|
89
|
+
"fuzzy",
|
|
90
|
+
expected_file_hash or current_hash,
|
|
91
|
+
current_hash,
|
|
92
|
+
fuzzy[3],
|
|
93
|
+
)
|
|
94
|
+
return _failed(str(path), "no_match", expected_file_hash or current_hash, "patch target not found")
|
|
69
95
|
|
|
70
96
|
|
|
71
97
|
def _normalize_newlines(content: str) -> str:
|
|
@@ -114,19 +140,48 @@ def _candidate_spans(text: str, reference: str, line_drift: int) -> list[tuple[i
|
|
|
114
140
|
|
|
115
141
|
def _best_fuzzy_span(text: str, reference: str) -> tuple[int, int, str, float, bool] | None:
|
|
116
142
|
best: tuple[int, int, str, float] | None = None
|
|
143
|
+
best_lines: tuple[int, int] | None = None
|
|
117
144
|
ambiguous = False
|
|
145
|
+
lines = text.splitlines(keepends=True)
|
|
146
|
+
offsets = [0]
|
|
147
|
+
for line in lines:
|
|
148
|
+
offsets.append(offsets[-1] + len(line))
|
|
118
149
|
for start, end, block in _candidate_spans(text, reference, MAX_LINE_DRIFT):
|
|
150
|
+
# compute line range [start_line, end_line) for overlap test
|
|
151
|
+
start_line = 0
|
|
152
|
+
while start_line < len(offsets) - 1 and offsets[start_line + 1] <= start:
|
|
153
|
+
start_line += 1
|
|
154
|
+
end_line = start_line
|
|
155
|
+
while end_line < len(offsets) - 1 and offsets[end_line] < end:
|
|
156
|
+
end_line += 1
|
|
119
157
|
score = SequenceMatcher(None, block, reference, autojunk=False).ratio()
|
|
120
158
|
if best is None or score > best[3]:
|
|
121
159
|
best = (start, end, block, score)
|
|
160
|
+
best_lines = (start_line, end_line)
|
|
122
161
|
ambiguous = False
|
|
123
162
|
elif score >= DEFAULT_FUZZY_THRESHOLD and best and abs(score - best[3]) < 0.015:
|
|
163
|
+
# only treat as ambiguous if a genuinely different (non-overlapping) location matches closely
|
|
164
|
+
if best_lines is not None and not (end_line <= best_lines[0] or start_line >= best_lines[1]):
|
|
165
|
+
continue # overlapping window on same location -> not real ambiguity
|
|
124
166
|
ambiguous = True
|
|
125
167
|
if best is None:
|
|
126
168
|
return None
|
|
127
169
|
return best[0], best[1], best[2], best[3], ambiguous
|
|
128
170
|
|
|
129
171
|
|
|
172
|
+
def _atomic_write(path: Path, content: str) -> None:
|
|
173
|
+
"""Atomic replace via temp in same dir + fsync to prevent corruption on crash."""
|
|
174
|
+
dir_ = path.parent
|
|
175
|
+
with tempfile.NamedTemporaryFile(
|
|
176
|
+
mode="w", encoding="utf-8", dir=dir_, delete=False, suffix=".tmp"
|
|
177
|
+
) as tmp:
|
|
178
|
+
tmp.write(content)
|
|
179
|
+
tmp.flush()
|
|
180
|
+
os.fsync(tmp.fileno())
|
|
181
|
+
tmp_path = Path(tmp.name)
|
|
182
|
+
os.replace(tmp_path, path)
|
|
183
|
+
|
|
184
|
+
|
|
130
185
|
def _commit(
|
|
131
186
|
path: Path,
|
|
132
187
|
text: str,
|
|
@@ -139,7 +194,7 @@ def _commit(
|
|
|
139
194
|
score: float,
|
|
140
195
|
) -> PatchResult:
|
|
141
196
|
updated = f"{text[:start]}{new_content}{text[end:]}"
|
|
142
|
-
path
|
|
197
|
+
_atomic_write(path, updated)
|
|
143
198
|
return PatchResult(True, str(path), strategy, "patch applied", expected, matched, round(score, 4), 1)
|
|
144
199
|
|
|
145
200
|
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import concurrent.futures
|
|
4
|
+
import contextlib
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from cluxion_agentplugin_supercoder.core.hash_patch import apply_patch, file_hash
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_exact_patch(tmp_path: Path) -> None:
|
|
15
|
+
path = tmp_path / "a.py"
|
|
16
|
+
path.write_text("alpha\nbeta\ngamma\n", encoding="utf-8")
|
|
17
|
+
expected = file_hash(path.read_text(encoding="utf-8"))
|
|
18
|
+
result = apply_patch(path, old_text="beta\n", new_text="BETA\n", expected_file_hash=expected)
|
|
19
|
+
assert result.success is True
|
|
20
|
+
assert "BETA" in path.read_text(encoding="utf-8")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_stale_patch_blocked(tmp_path: Path) -> None:
|
|
24
|
+
path = tmp_path / "a.py"
|
|
25
|
+
path.write_text("one\n", encoding="utf-8")
|
|
26
|
+
result = apply_patch(path, old_text="one\n", new_text="two\n", expected_file_hash="0" * 64)
|
|
27
|
+
assert result.success is False
|
|
28
|
+
assert "changed" in result.message
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_missing_file_fails_closed(tmp_path: Path) -> None:
|
|
32
|
+
result = apply_patch(tmp_path / "absent.py", old_text="x", new_text="y")
|
|
33
|
+
assert result.success is False
|
|
34
|
+
assert result.strategy == "missing_file"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_fuzzy_patch_tolerates_minor_drift(tmp_path: Path) -> None:
|
|
38
|
+
path = tmp_path / "a.py"
|
|
39
|
+
body = "def handler(request):\n value = compute(request)\n return value\n"
|
|
40
|
+
path.write_text(body, encoding="utf-8")
|
|
41
|
+
# old_text drifts from the file by one comment line the model forgot.
|
|
42
|
+
drifted = "def handler(request):\n value = compute(request) # cached\n return value\n"
|
|
43
|
+
result = apply_patch(path, old_text=drifted, new_text="def handler(request):\n return compute(request)\n")
|
|
44
|
+
assert result.success is True
|
|
45
|
+
assert result.strategy == "fuzzy"
|
|
46
|
+
assert 0.86 <= result.similarity < 1.0
|
|
47
|
+
assert path.read_text(encoding="utf-8") == "def handler(request):\n return compute(request)\n"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_low_similarity_is_no_match(tmp_path: Path) -> None:
|
|
51
|
+
path = tmp_path / "a.py"
|
|
52
|
+
path.write_text("alpha\nbeta\ngamma\n", encoding="utf-8")
|
|
53
|
+
result = apply_patch(path, old_text="completely different content\n", new_text="x\n")
|
|
54
|
+
assert result.success is False
|
|
55
|
+
assert result.strategy == "no_match"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_ambiguous_fuzzy_candidates_refuse_to_guess(tmp_path: Path) -> None:
|
|
59
|
+
# Two near-identical blocks: a fuzzy match could land on either, so the
|
|
60
|
+
# patch must fail instead of silently editing the wrong one.
|
|
61
|
+
path = tmp_path / "a.py"
|
|
62
|
+
block = "def f():\n return 1\n"
|
|
63
|
+
path.write_text(block + "\n" + block, encoding="utf-8")
|
|
64
|
+
near = "def f():\n return 2\n"
|
|
65
|
+
result = apply_patch(path, old_text=near, new_text="def f():\n return 3\n")
|
|
66
|
+
assert result.success is False
|
|
67
|
+
assert result.strategy == "no_match"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_exact_match_uses_first_occurrence(tmp_path: Path) -> None:
|
|
71
|
+
path = tmp_path / "a.py"
|
|
72
|
+
path.write_text("x = 1\ny = 2\nx = 1\n", encoding="utf-8")
|
|
73
|
+
result = apply_patch(path, old_text="x = 1\n", new_text="x = 9\n")
|
|
74
|
+
assert result.success is True
|
|
75
|
+
assert path.read_text(encoding="utf-8") == "x = 9\ny = 2\nx = 1\n"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_sha256_prefix_and_bad_hash_rejected(tmp_path: Path) -> None:
|
|
79
|
+
path = tmp_path / "a.py"
|
|
80
|
+
path.write_text("one\n", encoding="utf-8")
|
|
81
|
+
prefixed = "sha256:" + file_hash("one\n")
|
|
82
|
+
result = apply_patch(path, old_text="one\n", new_text="two\n", expected_file_hash=prefixed)
|
|
83
|
+
assert result.success is True
|
|
84
|
+
path.write_text("one\n", encoding="utf-8")
|
|
85
|
+
with pytest.raises(ValueError):
|
|
86
|
+
apply_patch(path, old_text="one\n", new_text="two\n", expected_file_hash="abc")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_fuzzy_short_2line_block_with_drift_applies(tmp_path: Path) -> None:
|
|
90
|
+
# regression: 2-line unique target with minor drift (trailing space + typo) must apply
|
|
91
|
+
# even though different window widths produce overlapping candidates
|
|
92
|
+
path = tmp_path / "multi.py"
|
|
93
|
+
body = """def foo():
|
|
94
|
+
x = 1
|
|
95
|
+
return x
|
|
96
|
+
|
|
97
|
+
def bar():
|
|
98
|
+
y = 2
|
|
99
|
+
return y
|
|
100
|
+
"""
|
|
101
|
+
path.write_text(body, encoding="utf-8")
|
|
102
|
+
# drifted old_text: trailing space on first line of block, one-char typo on second
|
|
103
|
+
drifted = " y = 2 \n return z\n"
|
|
104
|
+
new = " y = 42\n return y\n"
|
|
105
|
+
result = apply_patch(path, old_text=drifted, new_text=new)
|
|
106
|
+
assert result.success is True
|
|
107
|
+
assert result.strategy == "fuzzy"
|
|
108
|
+
content = path.read_text(encoding="utf-8")
|
|
109
|
+
assert "y = 42" in content
|
|
110
|
+
assert "return y" in content
|
|
111
|
+
assert "def bar():" in content
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_duplicate_blocks_still_ambiguous(tmp_path: Path) -> None:
|
|
115
|
+
# genuine duplicate (non-overlapping) must still refuse
|
|
116
|
+
path = tmp_path / "dups.py"
|
|
117
|
+
block = "def f():\n return 1\n"
|
|
118
|
+
path.write_text(block + "\n" + block, encoding="utf-8")
|
|
119
|
+
near = "def f():\n return 2\n"
|
|
120
|
+
result = apply_patch(path, old_text=near, new_text="def f():\n return 3\n")
|
|
121
|
+
assert result.success is False
|
|
122
|
+
assert result.strategy == "no_match"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# === Concurrency and atomicity tests ===
|
|
126
|
+
|
|
127
|
+
def _worker_apply(i: int, path: Path) -> bool:
|
|
128
|
+
"""Worker that applies a unique non-overlapping patch under lock."""
|
|
129
|
+
old = f"# UNIQUE_PATCH_{i}_START\n"
|
|
130
|
+
new = f"# UNIQUE_PATCH_{i}_DONE\n"
|
|
131
|
+
# distinct markers, no value chaining conflict
|
|
132
|
+
result = apply_patch(path, old_text=old, new_text=new)
|
|
133
|
+
return result.success
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_concurrent_patches_no_lost_update(tmp_path: Path) -> None:
|
|
137
|
+
"""8 concurrent workers on same file: lock serializes, no lost updates, final coherent."""
|
|
138
|
+
path = tmp_path / "concurrent.py"
|
|
139
|
+
# initial content with 8 distinct unique markers
|
|
140
|
+
initial = "\n".join(f"# UNIQUE_PATCH_{i}_START" for i in range(8)) + "\n"
|
|
141
|
+
path.write_text(initial, encoding="utf-8")
|
|
142
|
+
|
|
143
|
+
# each patch changes its unique marker; lock ensures serialization, all succeed
|
|
144
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor:
|
|
145
|
+
futures = [
|
|
146
|
+
executor.submit(_worker_apply, i, path) for i in range(8)
|
|
147
|
+
]
|
|
148
|
+
successes = [f.result() for f in concurrent.futures.as_completed(futures)]
|
|
149
|
+
|
|
150
|
+
assert all(successes), "Some patches lost due to race"
|
|
151
|
+
final = path.read_text(encoding="utf-8")
|
|
152
|
+
# final should have all DONE markers, no START left
|
|
153
|
+
for i in range(8):
|
|
154
|
+
assert f"# UNIQUE_PATCH_{i}_DONE" in final
|
|
155
|
+
assert f"# UNIQUE_PATCH_{i}_START" not in final
|
|
156
|
+
|
|
157
|
+
# hash chain consistent (changed from initial)
|
|
158
|
+
new_hash = file_hash(final)
|
|
159
|
+
assert new_hash != file_hash(initial)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_atomic_write_interruption_leaves_original_intact(tmp_path: Path) -> None:
|
|
163
|
+
"""Simulated mid-write crash leaves ORIGINAL file intact (temp may remain, no truncate)."""
|
|
164
|
+
path = tmp_path / "atomic_test.txt"
|
|
165
|
+
original = "IMPORTANT ORIGINAL CONTENT\nDO NOT LOSE\n"
|
|
166
|
+
path.write_text(original, encoding="utf-8")
|
|
167
|
+
orig_hash = file_hash(original)
|
|
168
|
+
|
|
169
|
+
# simulate crash mid atomic write by patching _atomic_write temporarily
|
|
170
|
+
def crashing_atomic(p: Path, content: str) -> None:
|
|
171
|
+
dir_ = p.parent
|
|
172
|
+
with tempfile.NamedTemporaryFile(
|
|
173
|
+
mode="w", encoding="utf-8", dir=dir_, delete=False, suffix=".tmp"
|
|
174
|
+
) as tmp:
|
|
175
|
+
tmp.write(content[:10]) # partial write
|
|
176
|
+
tmp.flush()
|
|
177
|
+
os.fsync(tmp.fileno())
|
|
178
|
+
# simulate kill before replace
|
|
179
|
+
raise RuntimeError("simulated crash mid-write")
|
|
180
|
+
|
|
181
|
+
original_atomic = None
|
|
182
|
+
try:
|
|
183
|
+
# monkey patch for test
|
|
184
|
+
import cluxion_agentplugin_supercoder.core.hash_patch as hp
|
|
185
|
+
|
|
186
|
+
original_atomic = hp._atomic_write
|
|
187
|
+
hp._atomic_write = crashing_atomic
|
|
188
|
+
with contextlib.suppress(RuntimeError):
|
|
189
|
+
hp._atomic_write(path, "CORRUPTED NEW CONTENT\n")
|
|
190
|
+
# after crash, original must be untouched
|
|
191
|
+
after = path.read_text(encoding="utf-8")
|
|
192
|
+
assert after == original, "Atomic write failed: original was corrupted or truncated"
|
|
193
|
+
assert file_hash(after) == orig_hash
|
|
194
|
+
finally:
|
|
195
|
+
import cluxion_agentplugin_supercoder.core.hash_patch as hp
|
|
196
|
+
|
|
197
|
+
if original_atomic is not None:
|
|
198
|
+
hp._atomic_write = original_atomic
|
|
199
|
+
# cleanup any leftover temp if test created
|
|
200
|
+
for f in tmp_path.glob("*.tmp"):
|
|
201
|
+
f.unlink(missing_ok=True)
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
import pytest
|
|
6
|
-
|
|
7
|
-
from cluxion_agentplugin_supercoder.core.hash_patch import apply_patch, file_hash
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def test_exact_patch(tmp_path: Path) -> None:
|
|
11
|
-
path = tmp_path / "a.py"
|
|
12
|
-
path.write_text("alpha\nbeta\ngamma\n", encoding="utf-8")
|
|
13
|
-
expected = file_hash(path.read_text(encoding="utf-8"))
|
|
14
|
-
result = apply_patch(path, old_text="beta\n", new_text="BETA\n", expected_file_hash=expected)
|
|
15
|
-
assert result.success is True
|
|
16
|
-
assert "BETA" in path.read_text(encoding="utf-8")
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def test_stale_patch_blocked(tmp_path: Path) -> None:
|
|
20
|
-
path = tmp_path / "a.py"
|
|
21
|
-
path.write_text("one\n", encoding="utf-8")
|
|
22
|
-
result = apply_patch(path, old_text="one\n", new_text="two\n", expected_file_hash="0" * 64)
|
|
23
|
-
assert result.success is False
|
|
24
|
-
assert "changed" in result.message
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def test_missing_file_fails_closed(tmp_path: Path) -> None:
|
|
28
|
-
result = apply_patch(tmp_path / "absent.py", old_text="x", new_text="y")
|
|
29
|
-
assert result.success is False
|
|
30
|
-
assert result.strategy == "missing_file"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def test_fuzzy_patch_tolerates_minor_drift(tmp_path: Path) -> None:
|
|
34
|
-
path = tmp_path / "a.py"
|
|
35
|
-
body = "def handler(request):\n value = compute(request)\n return value\n"
|
|
36
|
-
path.write_text(body, encoding="utf-8")
|
|
37
|
-
# old_text drifts from the file by one comment line the model forgot.
|
|
38
|
-
drifted = "def handler(request):\n value = compute(request) # cached\n return value\n"
|
|
39
|
-
result = apply_patch(path, old_text=drifted, new_text="def handler(request):\n return compute(request)\n")
|
|
40
|
-
assert result.success is True
|
|
41
|
-
assert result.strategy == "fuzzy"
|
|
42
|
-
assert 0.86 <= result.similarity < 1.0
|
|
43
|
-
assert path.read_text(encoding="utf-8") == "def handler(request):\n return compute(request)\n"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def test_low_similarity_is_no_match(tmp_path: Path) -> None:
|
|
47
|
-
path = tmp_path / "a.py"
|
|
48
|
-
path.write_text("alpha\nbeta\ngamma\n", encoding="utf-8")
|
|
49
|
-
result = apply_patch(path, old_text="completely different content\n", new_text="x\n")
|
|
50
|
-
assert result.success is False
|
|
51
|
-
assert result.strategy == "no_match"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def test_ambiguous_fuzzy_candidates_refuse_to_guess(tmp_path: Path) -> None:
|
|
55
|
-
# Two near-identical blocks: a fuzzy match could land on either, so the
|
|
56
|
-
# patch must fail instead of silently editing the wrong one.
|
|
57
|
-
path = tmp_path / "a.py"
|
|
58
|
-
block = "def f():\n return 1\n"
|
|
59
|
-
path.write_text(block + "\n" + block, encoding="utf-8")
|
|
60
|
-
near = "def f():\n return 2\n"
|
|
61
|
-
result = apply_patch(path, old_text=near, new_text="def f():\n return 3\n")
|
|
62
|
-
assert result.success is False
|
|
63
|
-
assert result.strategy == "no_match"
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def test_exact_match_uses_first_occurrence(tmp_path: Path) -> None:
|
|
67
|
-
path = tmp_path / "a.py"
|
|
68
|
-
path.write_text("x = 1\ny = 2\nx = 1\n", encoding="utf-8")
|
|
69
|
-
result = apply_patch(path, old_text="x = 1\n", new_text="x = 9\n")
|
|
70
|
-
assert result.success is True
|
|
71
|
-
assert path.read_text(encoding="utf-8") == "x = 9\ny = 2\nx = 1\n"
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def test_sha256_prefix_and_bad_hash_rejected(tmp_path: Path) -> None:
|
|
75
|
-
path = tmp_path / "a.py"
|
|
76
|
-
path.write_text("one\n", encoding="utf-8")
|
|
77
|
-
prefixed = "sha256:" + file_hash("one\n")
|
|
78
|
-
result = apply_patch(path, old_text="one\n", new_text="two\n", expected_file_hash=prefixed)
|
|
79
|
-
assert result.success is True
|
|
80
|
-
path.write_text("one\n", encoding="utf-8")
|
|
81
|
-
with pytest.raises(ValueError):
|
|
82
|
-
apply_patch(path, old_text="one\n", new_text="two\n", expected_file_hash="abc")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/ARCHITECTURE.md
RENAMED
|
File without changes
|
{cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/Docs/README.md
RENAMED
|
File without changes
|
{cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/Docs/agent-surfaces.md
RENAMED
|
File without changes
|
{cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/Docs/architecture.md
RENAMED
|
File without changes
|
{cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/Docs/capabilities.md
RENAMED
|
File without changes
|
{cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/Docs/design.md
RENAMED
|
File without changes
|
{cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/Docs/installation.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/tests/test_cursor.py
RENAMED
|
File without changes
|
{cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/tests/test_doctor.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/tests/test_plugin.py
RENAMED
|
File without changes
|
{cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/tests/test_queue.py
RENAMED
|
File without changes
|
{cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/tests/test_repo_map.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cluxion_agentplugin_supercoder-0.2.5 → cluxion_agentplugin_supercoder-0.2.7}/tests/test_safety.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|