Files
FESADev/tests/test_execute.py
T

282 lines
9.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import json
import subprocess
import pytest
from scripts import execute
def write_implementation_agent_profile(
root, *, model="gpt-5.6-terra", reasoning="xhigh"
):
profile = root / ".codex" / "agents" / "implementation-agent.toml"
profile.parent.mkdir(parents=True)
profile.write_text(
f'model = "{model}"\nmodel_reasoning_effort = "{reasoning}"\n',
encoding="utf-8",
)
def test_invoke_codex_uses_utf8_for_unicode_prompt(tmp_path, monkeypatch):
phase_dir = tmp_path / "phases" / "unicode-phase"
phase_dir.mkdir(parents=True)
(phase_dir / "index.json").write_text(
json.dumps(
{
"project": "FESA Structural Solver",
"phase": "unicode-phase",
"steps": [{"step": 0, "name": "unicode", "status": "pending"}],
}
),
encoding="utf-8",
)
(phase_dir / "step0.md").write_text("Euler–Bernoulli 보", encoding="utf-8")
write_implementation_agent_profile(tmp_path)
invocation = {}
def capture_run(command, **kwargs):
invocation["command"] = command
invocation["kwargs"] = kwargs
return subprocess.CompletedProcess(command, 0, stdout="", stderr="")
monkeypatch.setattr(execute, "ROOT", tmp_path)
monkeypatch.setattr(execute.subprocess, "run", capture_run)
executor = execute.StepExecutor("unicode-phase")
executor._invoke_codex({"step": 0, "name": "unicode"}, "승인된 설계 – ")
assert "Euler–Bernoulli 보" in invocation["kwargs"]["input"]
assert invocation["kwargs"]["encoding"] == "utf-8"
sandbox_index = invocation["command"].index("--sandbox")
assert invocation["command"][sandbox_index + 1] == "workspace-write"
def test_invoke_codex_uses_implementation_agent_command_contract(
tmp_path, monkeypatch
):
phase_dir = tmp_path / "phases" / "agent-phase"
phase_dir.mkdir(parents=True)
(phase_dir / "index.json").write_text(
json.dumps(
{
"project": "FESA Structural Solver",
"phase": "agent-phase",
"steps": [{"step": 0, "name": "agent", "status": "pending"}],
}
),
encoding="utf-8",
)
(phase_dir / "step0.md").write_text("agent routing", encoding="utf-8")
write_implementation_agent_profile(tmp_path)
invocation = {}
def capture_run(command, **kwargs):
invocation["command"] = command
return subprocess.CompletedProcess(command, 0, stdout="", stderr="")
monkeypatch.setattr(execute, "ROOT", tmp_path)
monkeypatch.setattr(execute.subprocess, "run", capture_run)
executor = execute.StepExecutor("agent-phase")
executor._invoke_codex({"step": 0, "name": "agent"}, "")
model_index = invocation["command"].index("--model")
config_index = invocation["command"].index("--config")
assert invocation["command"][model_index + 1] == "gpt-5.6-terra"
assert invocation["command"][config_index + 1] == 'model_reasoning_effort="xhigh"'
adjacent_arguments = list(zip(invocation["command"], invocation["command"][1:]))
assert ("--disable", "unified_exec") in adjacent_arguments
def test_invoke_codex_accepts_explicit_danger_full_access_override(
tmp_path, monkeypatch
):
phase_dir = tmp_path / "phases" / "sandbox-phase"
phase_dir.mkdir(parents=True)
(phase_dir / "index.json").write_text(
json.dumps(
{
"project": "FESA Structural Solver",
"phase": "sandbox-phase",
"steps": [{"step": 0, "name": "sandbox", "status": "pending"}],
}
),
encoding="utf-8",
)
(phase_dir / "step0.md").write_text("sandbox override", encoding="utf-8")
write_implementation_agent_profile(tmp_path)
invocation = {}
def capture_run(command, **kwargs):
invocation["command"] = command
return subprocess.CompletedProcess(command, 0, stdout="", stderr="")
monkeypatch.setattr(execute, "ROOT", tmp_path)
monkeypatch.setattr(execute.subprocess, "run", capture_run)
monkeypatch.setenv("FESA_HARNESS_CODEX_SANDBOX", "danger-full-access")
executor = execute.StepExecutor("sandbox-phase")
executor._invoke_codex({"step": 0, "name": "sandbox"}, "")
sandbox_index = invocation["command"].index("--sandbox")
assert invocation["command"][sandbox_index + 1] == "danger-full-access"
def test_invoke_codex_timeout_writes_structured_failure(tmp_path, monkeypatch):
phase_dir = tmp_path / "phases" / "timeout-phase"
phase_dir.mkdir(parents=True)
(phase_dir / "index.json").write_text(
json.dumps(
{
"project": "FESA Structural Solver",
"phase": "timeout-phase",
"steps": [{"step": 0, "name": "timeout", "status": "pending"}],
}
),
encoding="utf-8",
)
(phase_dir / "step0.md").write_text("timeout", encoding="utf-8")
write_implementation_agent_profile(tmp_path)
def timeout_run(command, **kwargs):
raise subprocess.TimeoutExpired(
command, kwargs["timeout"], output="partial stdout", stderr="partial stderr"
)
monkeypatch.setattr(execute, "ROOT", tmp_path)
monkeypatch.setattr(execute.subprocess, "run", timeout_run)
executor = execute.StepExecutor("timeout-phase")
output = executor._invoke_codex({"step": 0, "name": "timeout"}, "")
assert output["exitCode"] == 124
assert output["stdout"] == "partial stdout"
assert "Codex timed out after 1800 seconds." in output["stderr"]
stored = json.loads((phase_dir / "step0-output.json").read_text(encoding="utf-8"))
assert stored == output
def test_execute_single_step_retries_structured_codex_timeout(tmp_path, monkeypatch):
phase_dir = tmp_path / "phases" / "retry-phase"
phase_dir.mkdir(parents=True)
(phase_dir / "index.json").write_text(
json.dumps(
{
"project": "FESA Structural Solver",
"phase": "retry-phase",
"steps": [{"step": 0, "name": "retry", "status": "pending"}],
}
),
encoding="utf-8",
)
(phase_dir / "step0.md").write_text("retry", encoding="utf-8")
write_implementation_agent_profile(tmp_path)
attempts = []
def timeout_run(command, **kwargs):
attempts.append(command)
raise subprocess.TimeoutExpired(command, kwargs["timeout"])
monkeypatch.setattr(execute, "ROOT", tmp_path)
monkeypatch.setattr(execute.subprocess, "run", timeout_run)
executor = execute.StepExecutor("retry-phase")
executor.MAX_RETRIES = 2
monkeypatch.setattr(executor, "_commit_step", lambda *args: None)
monkeypatch.setattr(executor, "_update_top_index", lambda *args: None)
with pytest.raises(SystemExit, match="1"):
executor._execute_single_step(
{"step": 0, "name": "retry", "status": "pending"}, ""
)
assert len(attempts) == 2
index = json.loads((phase_dir / "index.json").read_text(encoding="utf-8"))
assert index["steps"][0]["status"] == "error"
assert "Codex timed out after 1800 seconds." in index["steps"][0]["error_message"]
def test_invoke_codex_rejects_unknown_sandbox_override(tmp_path, monkeypatch):
phase_dir = tmp_path / "phases" / "sandbox-phase"
phase_dir.mkdir(parents=True)
(phase_dir / "index.json").write_text(
json.dumps(
{
"project": "FESA Structural Solver",
"phase": "sandbox-phase",
"steps": [{"step": 0, "name": "sandbox", "status": "pending"}],
}
),
encoding="utf-8",
)
(phase_dir / "step0.md").write_text("sandbox override", encoding="utf-8")
monkeypatch.setattr(execute, "ROOT", tmp_path)
monkeypatch.setenv("FESA_HARNESS_CODEX_SANDBOX", "read-only")
executor = execute.StepExecutor("sandbox-phase")
with pytest.raises(execute.CodexEnvironmentError, match="FESA_HARNESS_CODEX_SANDBOX"):
executor._invoke_codex({"step": 0, "name": "sandbox"}, "")
def test_codex_environment_failure_detects_missing_sandbox_helper_on_zero_exit():
output = {
"exitCode": 0,
"stdout": "",
"stderr": (
"windows sandbox: orchestrator_helper_launch_failed: "
"helper=codex-windows-sandbox-setup.exe, error=program not found"
),
}
assert execute.StepExecutor._codex_environment_failure(output) == (
"Codex Windows sandbox helper를 실행할 수 없습니다."
)
def test_run_git_decodes_output_as_utf8(tmp_path, monkeypatch):
invocation = {}
def capture_run(command, **kwargs):
invocation["command"] = command
invocation["kwargs"] = kwargs
return subprocess.CompletedProcess(command, 0, stdout="", stderr="")
monkeypatch.setattr(execute.subprocess, "run", capture_run)
executor = object.__new__(execute.StepExecutor)
executor._root = str(tmp_path)
executor._run_git("status", "--short")
assert invocation["kwargs"]["encoding"] == "utf-8"
def test_feature_commit_message_is_windows_console_safe():
message = execute.StepExecutor.FEAT_MSG.format(
phase="linear-static-3d-euler-beam", num=0, name="requirements-baseline"
)
message.encode("cp949")
def test_configure_utf8_stdio_reconfigures_both_streams(monkeypatch):
class ReconfigurableStream:
def __init__(self):
self.encodings = []
def reconfigure(self, *, encoding):
self.encodings.append(encoding)
stdout = ReconfigurableStream()
stderr = ReconfigurableStream()
monkeypatch.setattr(execute.sys, "stdout", stdout)
monkeypatch.setattr(execute.sys, "stderr", stderr)
execute._configure_utf8_stdio()
assert stdout.encodings == ["utf-8"]
assert stderr.encodings == ["utf-8"]