239 lines
8.3 KiB
Python
239 lines
8.3 KiB
Python
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
from unittest.mock import Mock
|
|
|
|
from test_config import settings
|
|
|
|
from lai_automation.git import GitWorkspace
|
|
|
|
|
|
def test_prepare_uses_temporary_authentication_for_private_clone(tmp_path, monkeypatch) -> None:
|
|
workspace = GitWorkspace(settings(workspace_root=tmp_path, gitea_token="token"), 10)
|
|
calls = []
|
|
|
|
def run(command, **kwargs):
|
|
calls.append((command, kwargs))
|
|
if command[1] == "clone":
|
|
askpass = Path(kwargs["env"]["GIT_ASKPASS"])
|
|
assert askpass.is_file()
|
|
assert Path(kwargs["env"]["GIT_BOT_USER_FILE"]).read_text() == "bot"
|
|
assert Path(kwargs["env"]["GIT_BOT_TOKEN_FILE"]).read_text() == "token"
|
|
workspace.path.mkdir()
|
|
return subprocess.CompletedProcess(command, 0, "", "")
|
|
|
|
monkeypatch.setattr("lai_automation.git.subprocess.run", run)
|
|
|
|
workspace.prepare()
|
|
|
|
clone_command, clone_options = calls[0]
|
|
assert clone_command[:2] == ["git", "clone"]
|
|
assert "token" not in " ".join(clone_command)
|
|
assert clone_options["cwd"] == tmp_path
|
|
assert not tuple(tmp_path.glob(".lai-git-auth-*"))
|
|
|
|
|
|
def test_prepare_surfaces_clone_stderr_without_credentials(tmp_path, monkeypatch) -> None:
|
|
workspace = GitWorkspace(settings(workspace_root=tmp_path, gitea_token="token"), 11)
|
|
|
|
def run(command, **kwargs):
|
|
raise subprocess.CalledProcessError(
|
|
128,
|
|
command,
|
|
output="",
|
|
stderr="fatal: upstream returned 403",
|
|
)
|
|
|
|
monkeypatch.setattr("lai_automation.git.subprocess.run", run)
|
|
|
|
try:
|
|
workspace.prepare()
|
|
except RuntimeError as exception:
|
|
message = str(exception)
|
|
else:
|
|
raise AssertionError("Expected clone failure")
|
|
|
|
assert "Source repository clone failed" in message
|
|
assert "upstream returned 403" in message
|
|
assert "token" not in message
|
|
assert not tuple(tmp_path.glob(".lai-git-auth-*"))
|
|
|
|
|
|
def test_test_command_does_not_interpret_shell_metacharacters(tmp_path) -> None:
|
|
workspace = GitWorkspace(settings(workspace_root=tmp_path), 1)
|
|
workspace.path.mkdir()
|
|
|
|
result = workspace.run("printf safe ; touch injected", check=False)
|
|
|
|
assert result.stdout == "safe"
|
|
assert not (workspace.path / "injected").exists()
|
|
|
|
|
|
def test_extra_environment_is_passed_without_privilege_drop(tmp_path) -> None:
|
|
workspace = GitWorkspace(settings(workspace_root=tmp_path), 6)
|
|
workspace.path.mkdir()
|
|
|
|
result = workspace.run_args(
|
|
[sys.executable, "-c", "import os; print(os.environ['SAFE_TEST_VALUE'])"],
|
|
extra_environment={"SAFE_TEST_VALUE": "available"},
|
|
)
|
|
|
|
assert result.stdout.strip() == "available"
|
|
|
|
|
|
def test_coder_commands_use_native_process_identity(tmp_path, monkeypatch) -> None:
|
|
workspace = GitWorkspace(settings(workspace_root=tmp_path), 9)
|
|
workspace.path.mkdir()
|
|
captured = {}
|
|
|
|
def run(command, **kwargs):
|
|
captured.update(kwargs)
|
|
return subprocess.CompletedProcess(command, 0, "ok", "")
|
|
|
|
monkeypatch.setattr("lai_automation.git.subprocess.run", run)
|
|
monkeypatch.setattr(
|
|
"lai_automation.git.pwd.getpwnam",
|
|
lambda _: Mock(pw_dir="/home/coder", pw_uid=10001, pw_gid=10001),
|
|
)
|
|
|
|
workspace.run_args(["php", "-v"], username="coder")
|
|
|
|
assert captured["user"] == 10001
|
|
assert captured["group"] == 10001
|
|
assert captured["extra_groups"] == ()
|
|
assert "preexec_fn" not in captured
|
|
|
|
|
|
def test_summary_includes_untracked_files(tmp_path) -> None:
|
|
workspace = GitWorkspace(settings(workspace_root=tmp_path), 2)
|
|
workspace.path.mkdir()
|
|
subprocess.run(["git", "init"], cwd=workspace.path, check=True, capture_output=True)
|
|
(workspace.path / "new.php").write_text("<?php\n")
|
|
|
|
summary = workspace.summary()
|
|
|
|
assert summary.files == ("new.php",)
|
|
assert summary.added_lines == 1
|
|
|
|
|
|
def test_summary_includes_checkpoint_staged_files(tmp_path) -> None:
|
|
workspace = GitWorkspace(settings(workspace_root=tmp_path), 7)
|
|
workspace.path.mkdir()
|
|
subprocess.run(["git", "init"], cwd=workspace.path, check=True, capture_output=True)
|
|
tracked = workspace.path / "config.php"
|
|
tracked.write_text("<?php\nreturn [];\n")
|
|
subprocess.run(["git", "add", "."], cwd=workspace.path, check=True)
|
|
subprocess.run(
|
|
["git", "-c", "user.email=test@example.invalid", "-c", "user.name=Test",
|
|
"commit", "-m", "base"],
|
|
cwd=workspace.path,
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
tracked.write_text("<?php\nreturn ['fixed'];\n")
|
|
|
|
workspace.checkpoint()
|
|
summary = workspace.summary()
|
|
|
|
assert summary.files == ("config.php",)
|
|
assert summary.added_lines == 1
|
|
assert summary.deleted_lines == 1
|
|
|
|
|
|
def test_discard_changes_restores_clean_disposable_workspace(tmp_path) -> None:
|
|
workspace = GitWorkspace(settings(workspace_root=tmp_path), 3)
|
|
workspace.path.mkdir()
|
|
subprocess.run(["git", "init"], cwd=workspace.path, check=True, capture_output=True)
|
|
subprocess.run(
|
|
["git", "config", "user.email", "test@example.invalid"],
|
|
cwd=workspace.path,
|
|
check=True,
|
|
)
|
|
subprocess.run(
|
|
["git", "config", "user.name", "Test"],
|
|
cwd=workspace.path,
|
|
check=True,
|
|
)
|
|
tracked = workspace.path / "tracked.php"
|
|
tracked.write_text("<?php\nreturn [];\n")
|
|
subprocess.run(["git", "add", "."], cwd=workspace.path, check=True)
|
|
subprocess.run(["git", "commit", "-m", "base"], cwd=workspace.path, check=True, capture_output=True)
|
|
tracked.write_text("<?php\ninvalid\n")
|
|
untracked = workspace.path / "extra.php"
|
|
untracked.write_text("<?php\n")
|
|
|
|
workspace.discard_changes()
|
|
|
|
assert tracked.read_text() == "<?php\nreturn [];\n"
|
|
assert not untracked.exists()
|
|
|
|
|
|
def test_checkpoint_restores_last_valid_changes(tmp_path) -> None:
|
|
workspace = GitWorkspace(settings(workspace_root=tmp_path), 4)
|
|
workspace.path.mkdir()
|
|
subprocess.run(["git", "init"], cwd=workspace.path, check=True, capture_output=True)
|
|
subprocess.run(
|
|
["git", "config", "user.email", "test@example.invalid"],
|
|
cwd=workspace.path,
|
|
check=True,
|
|
)
|
|
subprocess.run(
|
|
["git", "config", "user.name", "Test"],
|
|
cwd=workspace.path,
|
|
check=True,
|
|
)
|
|
tracked = workspace.path / "config.php"
|
|
tracked.write_text("<?php\nreturn [];\n")
|
|
subprocess.run(["git", "add", "."], cwd=workspace.path, check=True)
|
|
subprocess.run(["git", "commit", "-m", "base"], cwd=workspace.path, check=True, capture_output=True)
|
|
tracked.write_text("<?php\nreturn ['valid'];\n")
|
|
added = workspace.path / "test.php"
|
|
added.write_text("<?php\n")
|
|
checkpoint = workspace.checkpoint()
|
|
tracked.write_text("<?php\nreturn [broken;\n")
|
|
|
|
workspace.restore_checkpoint(checkpoint)
|
|
|
|
assert tracked.read_text() == "<?php\nreturn ['valid'];\n"
|
|
assert added.exists()
|
|
|
|
|
|
def test_diff_returns_discarded_patch_excerpt(tmp_path) -> None:
|
|
workspace = GitWorkspace(settings(workspace_root=tmp_path), 5)
|
|
workspace.path.mkdir()
|
|
subprocess.run(["git", "init"], cwd=workspace.path, check=True, capture_output=True)
|
|
tracked = workspace.path / "service.php"
|
|
tracked.write_text("<?php\nreturn 'old';\n")
|
|
subprocess.run(["git", "add", "."], cwd=workspace.path, check=True)
|
|
subprocess.run(
|
|
["git", "-c", "user.email=test@example.invalid", "-c", "user.name=Test", "commit", "-m", "base"],
|
|
cwd=workspace.path,
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
tracked.write_text("<?php\nreturn 'new';\n")
|
|
|
|
patch = workspace.diff()
|
|
|
|
assert "-return 'old';" in patch
|
|
assert "+return 'new';" in patch
|
|
|
|
|
|
def test_head_file_reads_original_tracked_content(tmp_path) -> None:
|
|
workspace = GitWorkspace(settings(workspace_root=tmp_path), 8)
|
|
workspace.path.mkdir()
|
|
subprocess.run(["git", "init"], cwd=workspace.path, check=True, capture_output=True)
|
|
tracked = workspace.path / "config.php"
|
|
tracked.write_text("<?php\nreturn 'original';\n")
|
|
subprocess.run(["git", "add", "."], cwd=workspace.path, check=True)
|
|
subprocess.run(
|
|
["git", "-c", "user.email=test@example.invalid", "-c", "user.name=Test",
|
|
"commit", "-m", "base"],
|
|
cwd=workspace.path,
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
tracked.write_text("<?php\nreturn 'candidate';\n")
|
|
|
|
assert workspace.head_file("config.php") == "<?php\nreturn 'original';\n"
|
|
assert workspace.head_file("missing.php") is None |