Files
2026-07-30 13:22:03 +09:00

215 lines
5.9 KiB
Python

import os
import subprocess
import sys
from pathlib import Path
import pytest
import scripts.execute as execute_module
from scripts.execute import (
CodexEnvironmentError,
StepExecutor,
build_codex_command,
build_codex_environment,
configure_standard_streams,
normalize_codex_add_dirs,
run_utf8_process,
)
def test_normalize_codex_add_dirs_accepts_existing_absolute_directories(tmp_path):
first = tmp_path / "tool"
second = tmp_path / "runtime"
first.mkdir()
second.mkdir()
result = normalize_codex_add_dirs([str(first), str(second)])
assert result == (first.resolve(), second.resolve())
@pytest.mark.parametrize("raw", ["relative/tool", "missing"])
def test_normalize_codex_add_dirs_rejects_unsafe_paths(tmp_path, raw):
candidate = raw if raw.startswith("relative") else str(tmp_path / raw)
with pytest.raises(CodexEnvironmentError):
normalize_codex_add_dirs([candidate])
def test_build_codex_command_grants_only_explicit_directories(tmp_path):
root = tmp_path / "repo"
tool = tmp_path / "tool"
runtime = tmp_path / "runtime"
root.mkdir()
tool.mkdir()
runtime.mkdir()
codex = tmp_path / "codex.exe"
command = build_codex_command(
codex,
root,
(tool.resolve(), runtime.resolve()),
)
assert command == [
str(codex),
"exec",
"--json",
"--sandbox",
"workspace-write",
"--dangerously-bypass-hook-trust",
"--add-dir",
str(tool.resolve()),
"--add-dir",
str(runtime.resolve()),
"--cd",
str(root.resolve()),
"-",
]
def test_build_codex_environment_appends_tool_directories(tmp_path):
tool = (tmp_path / "tool").resolve()
runtime = (tmp_path / "runtime").resolve()
base = {"PATH": os.pathsep.join(("existing-one", "existing-two")), "KEEP": "value"}
result = build_codex_environment(base, (tool, runtime))
assert result["PATH"].split(os.pathsep) == [
"existing-one",
"existing-two",
str(tool),
str(runtime),
]
assert result["KEEP"] == "value"
assert result is not base
def test_run_utf8_process_sends_prompt_as_utf8(tmp_path):
verifier = tmp_path / "verify_utf8.py"
verifier.write_text(
"import sys\n"
"payload = sys.stdin.buffer.read()\n"
"text = payload.decode('utf-8')\n"
"sys.stdout.buffer.write(text.encode('utf-8'))\n",
encoding="utf-8",
)
prompt = "FESA 한글 prompt"
result = run_utf8_process(
[sys.executable, str(verifier)],
cwd=tmp_path,
prompt=prompt,
env=os.environ.copy(),
timeout=10,
)
assert result.returncode == 0
assert result.stdout == prompt
def test_invoke_codex_records_timeout_with_partial_output(tmp_path, monkeypatch):
phase_dir = tmp_path / "phases" / "test-phase"
phase_dir.mkdir(parents=True)
(phase_dir / "step0.md").write_text("# Timed step\n", encoding="utf-8")
codex = tmp_path / "codex.exe"
codex.touch()
executor = StepExecutor.__new__(StepExecutor)
executor._root = str(tmp_path)
executor._phase_dir = phase_dir
executor._codex_add_dirs = ()
monkeypatch.setattr(
execute_module.shutil, "which", lambda executable: str(codex)
)
def raise_timeout(*args, **kwargs):
raise subprocess.TimeoutExpired(
cmd=["codex", "exec"],
timeout=1800,
output=b"partial stdout",
stderr=b"partial stderr",
)
monkeypatch.setattr(execute_module, "run_utf8_process", raise_timeout)
output = executor._invoke_codex(
{"step": 0, "name": "timed-step"}, "preamble\n"
)
assert output["exitCode"] == 124
assert output["stdout"] == "partial stdout"
assert "partial stderr" in output["stderr"]
assert "timed out after 1800 seconds" in output["stderr"]
assert (
execute_module.json.loads(
(phase_dir / "step0-output.json").read_text(encoding="utf-8")
)
== output
)
def test_execute_single_step_retries_timeout_three_times(tmp_path, monkeypatch):
index_file = tmp_path / "index.json"
index_file.write_text(
execute_module.json.dumps(
{
"steps": [
{"step": 0, "name": "timed-step", "status": "pending"}
]
}
),
encoding="utf-8",
)
executor = StepExecutor.__new__(StepExecutor)
executor._index_file = index_file
executor._total = 1
executor._project = "FESA"
executor._phase_dir_name = "test-phase"
attempts = 0
def timeout_output(step, preamble):
nonlocal attempts
attempts += 1
return {
"step": 0,
"name": "timed-step",
"exitCode": 124,
"stdout": "",
"stderr": "Codex timed out after 1800 seconds.",
}
monkeypatch.setattr(executor, "_invoke_codex", timeout_output)
monkeypatch.setattr(executor, "_commit_step", lambda *args: None)
monkeypatch.setattr(executor, "_update_top_index", lambda *args: None)
with pytest.raises(SystemExit) as exit_info:
executor._execute_single_step(
{"step": 0, "name": "timed-step", "status": "pending"}, ""
)
assert exit_info.value.code == 1
assert attempts == 3
index = execute_module.json.loads(index_file.read_text(encoding="utf-8"))
assert index["steps"][0]["status"] == "error"
assert "timed out after 1800 seconds" in index["steps"][0]["error_message"]
def test_configure_standard_streams_replaces_unencodable_status_characters():
class RecordingStream:
def __init__(self):
self.calls = []
def reconfigure(self, **kwargs):
self.calls.append(kwargs)
stdout = RecordingStream()
stderr = RecordingStream()
configure_standard_streams(stdout, stderr)
assert stdout.calls == [{"errors": "replace"}]
assert stderr.calls == [{"errors": "replace"}]