modify documents

This commit is contained in:
KOKO\Mimi
2026-09-06 00:52:59 +09:00
parent 97e4d7a971
commit ac84235b7a
17 changed files with 2912 additions and 1195 deletions
+297 -9
View File
@@ -1,18 +1,33 @@
import json
import os
import subprocess
from types import SimpleNamespace
import pytest
from scripts import execute
@pytest.fixture(autouse=True)
def clear_sandbox_override(monkeypatch):
monkeypatch.delenv("FEM_HARNESS_CODEX_SANDBOX", raising=False)
def write_top_index(phase_dir):
(phase_dir.parent / "index.json").write_text(
json.dumps({"phases": [{"dir": phase_dir.name, "status": "pending"}]}),
encoding="utf-8",
)
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',
f'model = "{model}"\nmodel_reasoning_effort = "{reasoning}"\n'
'developer_instructions = """Implementation Agent test role."""\n',
encoding="utf-8",
)
@@ -20,6 +35,7 @@ def write_implementation_agent_profile(
def test_invoke_codex_uses_utf8_for_unicode_prompt(tmp_path, monkeypatch):
phase_dir = tmp_path / "phases" / "unicode-phase"
phase_dir.mkdir(parents=True)
write_top_index(phase_dir)
(phase_dir / "index.json").write_text(
json.dumps(
{
@@ -57,6 +73,7 @@ def test_invoke_codex_uses_implementation_agent_command_contract(
):
phase_dir = tmp_path / "phases" / "agent-phase"
phase_dir.mkdir(parents=True)
write_top_index(phase_dir)
(phase_dir / "index.json").write_text(
json.dumps(
{
@@ -95,6 +112,7 @@ def test_invoke_codex_accepts_explicit_danger_full_access_override(
):
phase_dir = tmp_path / "phases" / "sandbox-phase"
phase_dir.mkdir(parents=True)
write_top_index(phase_dir)
(phase_dir / "index.json").write_text(
json.dumps(
{
@@ -111,14 +129,23 @@ def test_invoke_codex_accepts_explicit_danger_full_access_override(
invocation = {}
def capture_run(command, **kwargs):
if command[0] == "git":
responses = {
("rev-parse", "--absolute-git-dir"): str(tmp_path / ".git/worktrees/sandbox"),
("rev-parse", "--git-common-dir"): str(tmp_path / ".git"),
("status", "--porcelain=v1", "-z", "--untracked-files=all"): "",
}
return subprocess.CompletedProcess(
command, 0, stdout=responses[tuple(command[1:])], stderr=""
)
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")
monkeypatch.setenv("FEM_HARNESS_CODEX_SANDBOX", "danger-full-access")
executor = execute.StepExecutor("sandbox-phase")
executor = execute.StepExecutor("sandbox-phase", danger_full_access_approved=True)
executor._invoke_codex({"step": 0, "name": "sandbox"}, "")
sandbox_index = invocation["command"].index("--sandbox")
@@ -128,6 +155,7 @@ def test_invoke_codex_accepts_explicit_danger_full_access_override(
def test_invoke_codex_timeout_writes_structured_failure(tmp_path, monkeypatch):
phase_dir = tmp_path / "phases" / "timeout-phase"
phase_dir.mkdir(parents=True)
write_top_index(phase_dir)
(phase_dir / "index.json").write_text(
json.dumps(
{
@@ -153,6 +181,7 @@ def test_invoke_codex_timeout_writes_structured_failure(tmp_path, monkeypatch):
output = executor._invoke_codex({"step": 0, "name": "timeout"}, "")
assert output["exitCode"] == 124
assert output["transportFailure"] == "timeout"
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"))
@@ -162,12 +191,13 @@ def test_invoke_codex_timeout_writes_structured_failure(tmp_path, monkeypatch):
def test_execute_single_step_retries_structured_codex_timeout(tmp_path, monkeypatch):
phase_dir = tmp_path / "phases" / "retry-phase"
phase_dir.mkdir(parents=True)
write_top_index(phase_dir)
(phase_dir / "index.json").write_text(
json.dumps(
{
"project": "FESA Structural Solver",
"phase": "retry-phase",
"steps": [{"step": 0, "name": "retry", "status": "pending"}],
"steps": [{"step": 0, "name": "retry", "status": "pending", "owned_paths": []}],
}
),
encoding="utf-8",
@@ -177,6 +207,15 @@ def test_execute_single_step_retries_structured_codex_timeout(tmp_path, monkeypa
attempts = []
def timeout_run(command, **kwargs):
if command[0] == "git":
responses = {
("symbolic-ref", "--quiet", "HEAD"): "refs/heads/feat-retry-phase",
("rev-parse", "--verify", "HEAD"): "a" * 40,
("status", "--porcelain=v1", "-z", "--untracked-files=all"): "",
}
return subprocess.CompletedProcess(
command, 0, stdout=responses[tuple(command[1:])], stderr=""
)
attempts.append(command)
raise subprocess.TimeoutExpired(command, kwargs["timeout"])
@@ -184,9 +223,6 @@ def test_execute_single_step_retries_structured_codex_timeout(tmp_path, monkeypa
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(
@@ -197,11 +233,15 @@ def test_execute_single_step_retries_structured_codex_timeout(tmp_path, monkeypa
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"]
assert index["steps"][0]["attempt_history"][-1]["reason"] == index["steps"][0]["error_message"]
top = json.loads((phase_dir.parent / "index.json").read_text(encoding="utf-8"))
assert top["phases"][0]["status"] == "error"
def test_invoke_codex_rejects_unknown_sandbox_override(tmp_path, monkeypatch):
phase_dir = tmp_path / "phases" / "sandbox-phase"
phase_dir.mkdir(parents=True)
write_top_index(phase_dir)
(phase_dir / "index.json").write_text(
json.dumps(
{
@@ -215,13 +255,59 @@ def test_invoke_codex_rejects_unknown_sandbox_override(tmp_path, monkeypatch):
(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")
monkeypatch.setenv("FEM_HARNESS_CODEX_SANDBOX", "read-only")
executor = execute.StepExecutor("sandbox-phase")
with pytest.raises(execute.CodexEnvironmentError, match="FESA_HARNESS_CODEX_SANDBOX"):
with pytest.raises(execute.CodexEnvironmentError, match="FEM_HARNESS_CODEX_SANDBOX"):
executor._invoke_codex({"step": 0, "name": "sandbox"}, "")
@pytest.mark.parametrize(
("approved", "isolated", "porcelain", "expected_error"),
[
(False, True, "", "--approve-danger-full-access"),
(True, False, "", "isolated linked worktree"),
(True, True, " M unrelated.md\0", "clean worktree"),
],
)
def test_sandbox_elevation_rejects_missing_safety_conditions(
tmp_path, monkeypatch, approved, isolated, porcelain, expected_error
):
phase_dir = tmp_path / "phases" / "sandbox-phase"
phase_dir.mkdir(parents=True)
write_top_index(phase_dir)
(phase_dir / "step0.md").write_text("sandbox guard", encoding="utf-8")
(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",
)
def git_probe(command, **kwargs):
assert command[0] == "git", "Rejected elevation must not launch Codex"
common_dir = tmp_path / ".git"
git_dir = common_dir / "worktrees/sandbox" if isolated else common_dir
responses = {
("rev-parse", "--absolute-git-dir"): str(git_dir),
("rev-parse", "--git-common-dir"): str(common_dir),
("status", "--porcelain=v1", "-z", "--untracked-files=all"): porcelain,
}
return subprocess.CompletedProcess(
command, 0, stdout=responses[tuple(command[1:])], stderr=""
)
monkeypatch.setattr(execute, "ROOT", tmp_path)
monkeypatch.setattr(execute.subprocess, "run", git_probe)
monkeypatch.setenv("FEM_HARNESS_CODEX_SANDBOX", "danger-full-access")
executor = execute.StepExecutor("sandbox-phase", danger_full_access_approved=approved)
with pytest.raises(execute.CodexEnvironmentError, match=expected_error):
executor._codex_sandbox_mode()
def test_codex_environment_failure_detects_missing_sandbox_helper_on_zero_exit():
output = {
"exitCode": 0,
@@ -279,3 +365,205 @@ def test_configure_utf8_stdio_reconfigures_both_streams(monkeypatch):
assert stdout.encodings == ["utf-8"]
assert stderr.encodings == ["utf-8"]
@pytest.fixture
def step_runtime(tmp_path, monkeypatch):
"""Run the real Step loop and Git against a disposable repo; replace only Codex."""
monkeypatch.setenv("GIT_CONFIG_GLOBAL", os.devnull)
monkeypatch.setenv("GIT_CONFIG_NOSYSTEM", "1")
monkeypatch.delenv("FEM_HARNESS_CODEX_SANDBOX", raising=False)
real_run = subprocess.run
def git(*args):
return real_run(
["git", *args], cwd=tmp_path, capture_output=True,
text=True, encoding="utf-8", check=True,
).stdout
git("init", "--quiet")
git("config", "user.name", "Harness Tests")
git("config", "user.email", "harness@example.invalid")
git("config", "commit.gpgsign", "false")
empty_hooks = tmp_path / ".git" / "empty-hooks"
empty_hooks.mkdir()
git("config", "core.hooksPath", str(empty_hooks))
phase_dir = tmp_path / "phases" / "runtime"
phase_dir.mkdir(parents=True)
write_top_index(phase_dir)
index_file = phase_dir / "index.json"
index_file.write_text(json.dumps({
"project": "Harness Tests", "phase": "runtime",
"steps": [{"step": 0, "name": "runtime", "status": "pending", "owned_paths": []}],
}), encoding="utf-8")
(phase_dir / "step0.md").write_text("Run the assigned test Step.", encoding="utf-8")
(tmp_path / ".gitignore").write_text("phases/**/step*-output.json\n", encoding="utf-8")
write_implementation_agent_profile(tmp_path)
git("add", "--", ".")
git("commit", "--quiet", "-m", "test: initialize disposable repo")
monkeypatch.setattr(execute, "ROOT", tmp_path)
runtime = SimpleNamespace(
executor=execute.StepExecutor("runtime"), root=tmp_path,
index_file=index_file, git=git, real_run=real_run, responses=[], calls=[],
)
def invoke(command, **kwargs):
if command[0] == "git":
return real_run(command, **kwargs)
assert command[0] == "codex", "Only the simulated Codex process is allowed"
runtime.calls.append(kwargs["input"])
response = runtime.responses.pop(0)
if callable(response):
response = response(runtime)
if isinstance(response, Exception):
raise response
index = json.loads(index_file.read_text(encoding="utf-8"))
current = index["steps"][0]
current["status"] = response.get("status", "pending")
if current["status"] != "pending":
field = {"completed": "summary", "error": "error_message", "blocked": "blocked_reason"}[current["status"]]
current[field] = response.get("reason", "reported reason")
index_file.write_text(json.dumps(index), encoding="utf-8")
return subprocess.CompletedProcess(
command, response.get("exit", 0),
stdout=response.get("stdout", ""), stderr=response.get("stderr", ""),
)
monkeypatch.setattr(execute.subprocess, "run", invoke)
return runtime
def run_step(runtime):
index = runtime.executor._read_phase_index()
return runtime.executor._execute_single_step(index["steps"][0], "")
@pytest.mark.parametrize("status,expected_exit", [("error", 1), ("blocked", 2)])
@pytest.mark.parametrize("process_exit", [0, 1, 124])
def test_explicit_failure_preserves_reason_without_retry(step_runtime, status, expected_exit, process_exit):
runtime = step_runtime
runtime.responses = [
{"status": status, "reason": "original failure", "exit": process_exit, "stderr": "process diagnostic"},
{"status": "completed"},
]
with pytest.raises(SystemExit) as stopped:
run_step(runtime)
assert stopped.value.code == expected_exit
assert len(runtime.calls) == 1
current = runtime.executor._read_phase_index()["steps"][0]
assert current["status"] == status
assert current["error_message" if status == "error" else "blocked_reason"] == "original failure"
assert current["attempt_history"][0]["reported_reason"] == "original failure"
assert current["attempt_history"][0]["reason"] == "original failure"
assert current["attempt_history"][0]["exit_code"] == process_exit
assert current["attempt_history"][0]["diagnostic"] == "process diagnostic"
assert runtime.executor._read_top_index()["phases"][0]["status"] == status
assert runtime.git("rev-list", "--count", "HEAD").strip() == "1"
@pytest.mark.parametrize("process_exit", [0, 1])
@pytest.mark.parametrize("status", ["pending", "completed", "error"])
def test_environment_failure_blocks_independent_of_exit_and_report(step_runtime, process_exit, status):
runtime = step_runtime
diagnostic = "windows sandbox: orchestrator_helper_launch_failed: helper not found"
runtime.responses = [{"status": status, "exit": process_exit, "stderr": diagnostic}]
with pytest.raises(SystemExit) as stopped:
run_step(runtime)
assert stopped.value.code == 2
assert len(runtime.calls) == 1
current = runtime.executor._read_phase_index()["steps"][0]
assert current["status"] == "blocked"
assert "sandbox helper" in current["blocked_reason"]
record = current["attempt_history"][0]
assert record["reported_status"] == status
assert record["classification"] == "environment"
assert record["diagnostic"] == diagnostic
assert runtime.executor._read_top_index()["phases"][0]["status"] == "blocked"
assert runtime.git("rev-list", "--count", "HEAD").strip() == "1"
def test_missing_cli_is_persisted_as_environment_block(step_runtime):
runtime = step_runtime
runtime.responses = [FileNotFoundError("codex")]
with pytest.raises(SystemExit) as stopped:
run_step(runtime)
assert stopped.value.code == 2
current = runtime.executor._read_phase_index()["steps"][0]
assert current["status"] == "blocked"
assert current["attempt_history"][0]["exit_code"] is None
assert runtime.executor._read_top_index()["phases"][0]["status"] == "blocked"
assert len(runtime.calls) == 1
@pytest.mark.parametrize("status,process_exit", [("pending", 0), ("pending", 1), ("pending", 124), ("completed", 1)])
def test_unclassified_or_inconsistent_result_never_retries(step_runtime, status, process_exit):
runtime = step_runtime
runtime.responses = [{"status": status, "exit": process_exit}, {"status": "completed"}]
with pytest.raises(SystemExit) as stopped:
run_step(runtime)
assert stopped.value.code == 1
assert len(runtime.calls) == 1
assert runtime.executor._read_phase_index()["steps"][0]["status"] == "error"
assert runtime.git("rev-list", "--count", "HEAD").strip() == "1"
def test_timeout_retry_preserves_history_and_can_complete(step_runtime):
runtime = step_runtime
runtime.responses = [subprocess.TimeoutExpired("codex", 1800), {"status": "completed"}]
assert run_step(runtime)
current = runtime.executor._read_phase_index()["steps"][0]
assert current["status"] == "completed"
assert [record["classification"] for record in current["attempt_history"]] == ["transport-timeout", "completed"]
assert [record["attempt"] for record in current["attempt_history"]] == [1, 2]
assert "timed out" in runtime.calls[1]
assert runtime.git("status", "--porcelain").strip() == ""
def test_history_survives_assigned_pending_reset(step_runtime):
runtime = step_runtime
runtime.responses = [{"status": "error", "reason": "original failure"}]
with pytest.raises(SystemExit):
run_step(runtime)
index = runtime.executor._read_phase_index()
history = index["steps"][0]["attempt_history"]
index["steps"][0]["status"] = "pending"
runtime.executor._write_json(runtime.index_file, index)
runtime.executor._ensure_created_at()
current = runtime.executor._read_phase_index()["steps"][0]
assert "error_message" not in current
assert current["attempt_history"] == history
runtime.responses = [{"status": "completed"}]
assert run_step(runtime)
assert [record["attempt"] for record in runtime.executor._read_phase_index()["steps"][0]["attempt_history"]] == [1, 2]
def test_full_runner_completes_metadata_only_phase(step_runtime):
runtime = step_runtime
runtime.responses = [{"status": "completed"}]
runtime.executor.run()
assert len(runtime.calls) == 1
assert runtime.executor._read_top_index()["phases"][0]["status"] == "completed"
assert "completed_at" in runtime.executor._read_phase_index()
assert runtime.git("symbolic-ref", "HEAD").strip() == "refs/heads/feat-runtime"
assert runtime.git("status", "--porcelain").strip() == ""
@pytest.mark.parametrize("status", ["error", "blocked", "completed"])
def test_terminal_report_prevents_retry_even_when_wrapper_times_out(step_runtime, status):
runtime = step_runtime
def report_then_timeout(runtime):
index = runtime.executor._read_phase_index()
current = index["steps"][0]
current["status"] = status
field = {"error": "error_message", "blocked": "blocked_reason", "completed": "summary"}[status]
current[field] = "original terminal report"
runtime.executor._write_json(runtime.index_file, index)
return subprocess.TimeoutExpired("codex", 1800)
runtime.responses = [report_then_timeout]
with pytest.raises(SystemExit) as stopped:
run_step(runtime)
assert stopped.value.code == (2 if status == "blocked" else 1)
assert len(runtime.calls) == 1
assert runtime.executor._read_phase_index()["steps"][0]["attempt_history"][0]["reported_reason"] == "original terminal report"