218 lines
6.6 KiB
Python
218 lines
6.6 KiB
Python
from __future__ import annotations
|
|
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
from opensquilla.tools.mutation_receipts import (
|
|
fingerprint_file,
|
|
record_semantic_mutation_receipt,
|
|
)
|
|
from opensquilla.tools.types import ToolContext, current_tool_context
|
|
|
|
|
|
def _run_git(workspace: Path, *args: str) -> None:
|
|
subprocess.run(
|
|
["git", *args],
|
|
cwd=workspace,
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
|
|
|
|
def _init_git_workspace(workspace: Path, relative_path: str = "src/app.py") -> Path:
|
|
target = workspace / relative_path
|
|
target.parent.mkdir(parents=True)
|
|
target.write_text("before\n", encoding="utf-8")
|
|
_run_git(workspace, "init")
|
|
_run_git(workspace, "config", "user.email", "test@example.com")
|
|
_run_git(workspace, "config", "user.name", "Test User")
|
|
_run_git(workspace, "add", ".")
|
|
_run_git(workspace, "commit", "-m", "init")
|
|
return target
|
|
|
|
|
|
def test_changed_source_mutation_receipt_increments_workspace_epoch(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
workspace = tmp_path / "workspace"
|
|
target = workspace / "src" / "app.py"
|
|
target.parent.mkdir(parents=True)
|
|
target.write_text("print('before')\n")
|
|
before = fingerprint_file(target)
|
|
target.write_text("print('after')\n")
|
|
after = fingerprint_file(target)
|
|
|
|
events: list[dict] = []
|
|
ctx = ToolContext(
|
|
workspace_dir=str(workspace),
|
|
on_runtime_event=events.append,
|
|
session_key="agent:main:test",
|
|
)
|
|
token = current_tool_context.set(ctx)
|
|
try:
|
|
receipt = record_semantic_mutation_receipt(
|
|
tool_name="edit_file",
|
|
path=target,
|
|
operation="edit_file",
|
|
before=before,
|
|
after=after,
|
|
partial=False,
|
|
)
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
|
|
assert receipt is not None
|
|
assert receipt["changed"] is True
|
|
assert receipt["workspace_epoch"] == 1
|
|
assert receipt["relative_path"] == "src/app.py"
|
|
assert receipt["classification"] == "source"
|
|
assert ctx.workspace_epoch == 1
|
|
assert ctx.workspace_mutation_receipts == [receipt]
|
|
assert events[0]["name"] == "workspace.semantic_mutation_receipt"
|
|
|
|
|
|
def test_changed_source_mutation_receipt_captures_candidate(tmp_path: Path) -> None:
|
|
workspace = tmp_path / "workspace"
|
|
target = _init_git_workspace(workspace)
|
|
before = fingerprint_file(target)
|
|
target.write_text("after\n", encoding="utf-8")
|
|
after = fingerprint_file(target)
|
|
|
|
ctx = ToolContext(workspace_dir=str(workspace))
|
|
token = current_tool_context.set(ctx)
|
|
try:
|
|
receipt = record_semantic_mutation_receipt(
|
|
tool_name="edit_source",
|
|
path=target,
|
|
operation="edit_source",
|
|
before=before,
|
|
after=after,
|
|
partial=False,
|
|
)
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
|
|
assert receipt is not None
|
|
assert receipt["receipt_id"].startswith("mut-")
|
|
assert len(ctx.source_diff_candidates) == 1
|
|
assert ctx.source_diff_candidates[0]["paths"] == ["src/app.py"]
|
|
assert ctx.source_diff_candidates[0]["receipt_id"] == receipt["receipt_id"]
|
|
|
|
|
|
def test_noop_mutation_receipt_does_not_increment_workspace_epoch(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
workspace = tmp_path / "workspace"
|
|
target = workspace / "src" / "app.py"
|
|
target.parent.mkdir(parents=True)
|
|
target.write_text("print('same')\n")
|
|
before = fingerprint_file(target)
|
|
after = fingerprint_file(target)
|
|
|
|
ctx = ToolContext(workspace_dir=str(workspace))
|
|
token = current_tool_context.set(ctx)
|
|
try:
|
|
receipt = record_semantic_mutation_receipt(
|
|
tool_name="write_file",
|
|
path=target,
|
|
operation="write_file",
|
|
before=before,
|
|
after=after,
|
|
partial=False,
|
|
)
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
|
|
assert receipt is not None
|
|
assert receipt["changed"] is False
|
|
assert receipt["workspace_epoch"] == 0
|
|
assert ctx.workspace_epoch == 0
|
|
assert ctx.source_diff_candidates == []
|
|
|
|
|
|
def test_test_like_mutation_receipt_does_not_capture_candidate(tmp_path: Path) -> None:
|
|
workspace = tmp_path / "workspace"
|
|
target = _init_git_workspace(workspace, "tests/test_app.py")
|
|
before = fingerprint_file(target)
|
|
target.write_text("after\n", encoding="utf-8")
|
|
after = fingerprint_file(target)
|
|
|
|
ctx = ToolContext(workspace_dir=str(workspace))
|
|
token = current_tool_context.set(ctx)
|
|
try:
|
|
receipt = record_semantic_mutation_receipt(
|
|
tool_name="edit_source",
|
|
path=target,
|
|
operation="edit_source",
|
|
before=before,
|
|
after=after,
|
|
partial=False,
|
|
)
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
|
|
assert receipt is not None
|
|
assert receipt["classification"] == "test-like"
|
|
assert ctx.source_diff_candidates == []
|
|
|
|
|
|
def test_mutation_receipt_returns_none_outside_workspace(tmp_path: Path) -> None:
|
|
workspace = tmp_path / "workspace"
|
|
outside = tmp_path / "outside.py"
|
|
workspace.mkdir()
|
|
outside.write_text("print('outside')\n")
|
|
before = fingerprint_file(outside)
|
|
after = fingerprint_file(outside)
|
|
|
|
ctx = ToolContext(workspace_dir=str(workspace))
|
|
token = current_tool_context.set(ctx)
|
|
try:
|
|
receipt = record_semantic_mutation_receipt(
|
|
tool_name="write_file",
|
|
path=outside,
|
|
operation="write_file",
|
|
before=before,
|
|
after=after,
|
|
partial=False,
|
|
)
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
|
|
assert receipt is None
|
|
assert ctx.workspace_mutation_receipts == []
|
|
assert ctx.workspace_epoch == 0
|
|
|
|
|
|
def test_mutation_receipt_swallows_runtime_event_exceptions(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
workspace = tmp_path / "workspace"
|
|
target = workspace / "src" / "app.py"
|
|
target.parent.mkdir(parents=True)
|
|
target.write_text("print('before')\n")
|
|
before = fingerprint_file(target)
|
|
target.write_text("print('after')\n")
|
|
after = fingerprint_file(target)
|
|
|
|
def raise_on_event(_event: dict) -> None:
|
|
raise RuntimeError("boom")
|
|
|
|
ctx = ToolContext(workspace_dir=str(workspace), on_runtime_event=raise_on_event)
|
|
token = current_tool_context.set(ctx)
|
|
try:
|
|
receipt = record_semantic_mutation_receipt(
|
|
tool_name="edit_file",
|
|
path=target,
|
|
operation="edit_file",
|
|
before=before,
|
|
after=after,
|
|
partial=False,
|
|
)
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
|
|
assert receipt is not None
|
|
assert receipt["changed"] is True
|
|
assert ctx.workspace_epoch == 1
|
|
assert ctx.workspace_mutation_receipts == [receipt]
|