Files
2026-07-13 13:12:33 +08:00

221 lines
7.1 KiB
Python

from __future__ import annotations
import json
import subprocess
from pathlib import Path
import pytest
from opensquilla.sandbox.config import SandboxSettings
from opensquilla.sandbox.integration import configure_runtime, reset_runtime
from opensquilla.tools.builtin.code_exec import execute_code
from opensquilla.tools.builtin.shell import exec_command
from opensquilla.tools.types import CallerKind, ToolContext, current_tool_context
def _init_git_workspace(path: Path) -> None:
subprocess.run(["git", "init"], cwd=path, check=True, stdout=subprocess.DEVNULL)
subprocess.run(
["git", "config", "user.email", "test@example.com"],
cwd=path,
check=True,
)
subprocess.run(["git", "config", "user.name", "Test User"], cwd=path, check=True)
@pytest.fixture
def mutation_context(tmp_path: Path):
reset_runtime()
workspace = tmp_path / "workspace"
workspace.mkdir()
_init_git_workspace(workspace)
scratch = tmp_path / "scratch"
scratch.mkdir()
configure_runtime(
SandboxSettings(
sandbox=False,
security_grading=False,
allow_legacy_mode=True,
),
workspace=workspace,
)
events: list[dict] = []
ctx = ToolContext(
is_owner=True,
caller_kind=CallerKind.AGENT,
workspace_dir=str(workspace),
scratch_dir=str(scratch),
session_key="agent:main:test",
on_runtime_event=events.append,
)
token = current_tool_context.set(ctx)
try:
yield workspace, scratch, ctx, events
finally:
current_tool_context.reset(token)
reset_runtime()
@pytest.mark.asyncio
async def test_exec_command_observes_workspace_mutation_without_model_visible_write(
mutation_context,
) -> None:
workspace, _scratch, ctx, events = mutation_context
result = await exec_command(
"mkdir -p src && printf 'print(1)\\n' > src/app.py",
workdir=str(workspace),
)
assert result.startswith("exit_code=0")
assert ctx.workspace_file_writes == []
assert ctx.workspace_mutation_records
assert ctx.workspace_mutation_records[0]["tool"] == "exec_command"
assert "shell_source_mutation_suspected" not in ctx.workspace_mutation_records[0]
assert ctx.workspace_mutation_records[0]["paths"] == [
{
"relative_path": "src/app.py",
"status": "??",
"classification": "source",
}
]
mutation_events = [
event for event in events if event.get("name") == "workspace_mutation_observed"
]
assert len(mutation_events) == 1
event = mutation_events[0]
assert event["feature"] == "mutation_ledger"
assert event["tool"] == "exec_command"
assert event["operation"] == "observe_only"
assert event["path_count"] == 1
assert event["paths"] == [
{
"relative_path": "src/app.py",
"status": "??",
"classification": "source",
}
]
assert "command_hash" in event
assert "command" not in event
@pytest.mark.asyncio
async def test_exec_command_observes_suspected_shell_source_mutation(
mutation_context,
) -> None:
workspace, _scratch, ctx, events = mutation_context
ctx.allowed_tools = {
"read_source",
"edit_source",
"source_symbols",
"grep_search",
"glob_search",
"git_status",
"git_diff",
"retrieve_tool_result",
"exec_command",
}
result = await exec_command(
"mkdir -p src && printf 'print(1)\\n' > src/app.py",
workdir=str(workspace),
)
assert result.startswith("exit_code=0")
assert ctx.workspace_mutation_records[0]["shell_source_mutation_suspected"] is True
assert ctx.workspace_mutation_records[0]["shell_source_mutation_targets"] == ["src/app.py"]
suspected_events = [
event for event in events if event.get("name") == "shell.source_mutation_suspected"
]
assert len(suspected_events) == 1
event = suspected_events[0]
assert event["feature"] == "shell_source_mutation"
assert event["tool"] == "exec_command"
assert event["shell_source_mutation_reason"] == "redirection_or_tee"
assert event["shell_source_mutation_targets"] == ["src/app.py"]
assert "command_hash" in event
assert "command" not in event
@pytest.mark.asyncio
async def test_exec_command_read_only_does_not_emit_mutation_event(
mutation_context,
) -> None:
workspace, _scratch, _ctx, events = mutation_context
result = await exec_command("git status --short", workdir=str(workspace))
assert result.startswith("exit_code=0")
assert not [event for event in events if event.get("name") == "workspace_mutation_observed"]
@pytest.mark.asyncio
async def test_exec_command_python_read_does_not_emit_source_mutation_signal(
mutation_context,
) -> None:
workspace, _scratch, _ctx, events = mutation_context
(workspace / "src").mkdir()
(workspace / "src" / "app.py").write_text("print(1)\n", encoding="utf-8")
result = await exec_command(
"python -c \"open('src/app.py').read()\"",
workdir=str(workspace),
)
assert result.startswith("exit_code=0")
assert not [event for event in events if event.get("name") == "shell.source_mutation_suspected"]
@pytest.mark.asyncio
async def test_execute_code_observes_workspace_mutation_classification(
mutation_context,
) -> None:
workspace, _scratch, ctx, events = mutation_context
result = await execute_code(
"from pathlib import Path\n"
"Path('tests/test_demo.py').parent.mkdir(exist_ok=True)\n"
"Path('tests/test_demo.py').write_text('def test_demo(): pass\\n')\n",
timeout=5,
)
payload = json.loads(result)
assert payload["exit_code"] == 0
assert ctx.workspace_file_writes == []
assert ctx.workspace_mutation_records
assert ctx.workspace_mutation_records[0]["paths"] == [
{
"relative_path": "tests/test_demo.py",
"status": "??",
"classification": "test-like",
}
]
mutation_events = [
event for event in events if event.get("name") == "workspace_mutation_observed"
]
assert len(mutation_events) == 1
event = mutation_events[0]
assert event["tool"] == "execute_code"
assert event["paths"] == [
{
"relative_path": "tests/test_demo.py",
"status": "??",
"classification": "test-like",
}
]
assert "code_hash" in event
assert "code" not in event
@pytest.mark.asyncio
async def test_exec_command_blocks_root_repro_files_as_scratch_artifacts(
mutation_context,
) -> None:
workspace, _scratch, ctx, events = mutation_context
result = await exec_command(
"printf 'temporary repro\\n' > debug_case.php",
workdir=str(workspace),
)
payload = json.loads(result)
assert payload["status"] == "blocked"
assert payload["reason"] == "workspace_scratch_artifact"
assert payload["path"] == "debug_case.php"
assert ctx.workspace_mutation_records == []
assert not [event for event in events if event.get("name") == "workspace_mutation_observed"]