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"]