201 lines
6.8 KiB
Python
201 lines
6.8 KiB
Python
import io
|
|
import json
|
|
import subprocess
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
from scripts import execute
|
|
from scripts.hooks import stop_validation
|
|
from scripts.msvc_harness.models import (
|
|
CommandSpec,
|
|
ProjectKind,
|
|
ValidationPlan,
|
|
ValidationStep,
|
|
)
|
|
from scripts.msvc_harness.process import ValidationFailure, execute_plan
|
|
|
|
|
|
def WriteExecutor(tmp_path, monkeypatch):
|
|
phase_dir = tmp_path / "phases" / "timeout-phase"
|
|
phase_dir.mkdir(parents=True)
|
|
(phase_dir.parent / "index.json").write_text(
|
|
json.dumps({"phases": [{"dir": "timeout-phase", "status": "pending"}]}),
|
|
encoding="utf-8",
|
|
)
|
|
(phase_dir / "index.json").write_text(
|
|
json.dumps(
|
|
{
|
|
"project": "Harness Tests",
|
|
"phase": "timeout-phase",
|
|
"steps": [{"step": 0, "name": "timeout", "status": "pending"}],
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
(phase_dir / "step0.md").write_text("timeout", encoding="utf-8")
|
|
profile = tmp_path / ".codex" / "agents" / "implementation-agent.toml"
|
|
profile.parent.mkdir(parents=True)
|
|
profile.write_text(
|
|
'model = "gpt-5.6-terra"\n'
|
|
'model_reasoning_effort = "xhigh"\n'
|
|
'developer_instructions = """test"""\n',
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setattr(execute, "ROOT", tmp_path)
|
|
return execute.StepExecutor("timeout-phase")
|
|
|
|
|
|
def TestClock(values):
|
|
iterator = iter(values)
|
|
return lambda: next(iterator)
|
|
|
|
|
|
def test_executor_uses_finite_default_for_actual_timeout(tmp_path, monkeypatch):
|
|
executor = WriteExecutor(tmp_path, monkeypatch)
|
|
timeouts = []
|
|
|
|
def timeout_run(command, **kwargs):
|
|
timeouts.append(kwargs["timeout"])
|
|
raise subprocess.TimeoutExpired(command, kwargs["timeout"])
|
|
|
|
monkeypatch.setattr(execute.subprocess, "run", timeout_run)
|
|
output = executor._invoke_codex({"step": 0, "name": "timeout"}, "")
|
|
|
|
assert execute.StepExecutor.CODEX_TIMEOUT_SECONDS == 14400
|
|
assert timeouts == [14400]
|
|
assert output["transportFailure"] == "timeout"
|
|
assert "Codex timed out after 14400 seconds." in output["stderr"]
|
|
|
|
|
|
def test_stop_passes_one_shared_deadline_to_root_toolchain_and_plan(
|
|
tmp_path, monkeypatch
|
|
):
|
|
root = tmp_path
|
|
selection = SimpleNamespace(kind=stop_validation.ProjectKind.CMAKE)
|
|
calls = {}
|
|
|
|
monkeypatch.setattr(stop_validation, "load_config", lambda _: object())
|
|
monkeypatch.setattr(
|
|
stop_validation,
|
|
"discover_project",
|
|
lambda _, __: SimpleNamespace(selection=selection),
|
|
)
|
|
|
|
def discover_toolchain(kind, *, deadline, clock):
|
|
calls["toolchain"] = (kind, deadline, clock())
|
|
return object()
|
|
|
|
class Adapter:
|
|
def create_plan(self, *args):
|
|
return object()
|
|
|
|
def capture_plan(plan, plan_root, *, env, deadline, clock):
|
|
calls["plan"] = (plan, plan_root, env, deadline, clock())
|
|
|
|
monkeypatch.setattr(stop_validation, "discover_toolchain", discover_toolchain)
|
|
monkeypatch.setattr(stop_validation, "CMakeAdapter", Adapter)
|
|
monkeypatch.setattr(stop_validation, "MSBuildAdapter", Adapter)
|
|
monkeypatch.setattr(stop_validation, "execute_plan", capture_plan)
|
|
|
|
clock = lambda: 100.0
|
|
assert stop_validation.run_validations(root, clock=clock) == (True, "")
|
|
assert stop_validation.TOTAL_TIMEOUT_SECONDS == 7200
|
|
assert calls["toolchain"][0].value == ProjectKind.CMAKE.value
|
|
assert calls["toolchain"][1] == 7300.0
|
|
assert calls["plan"][1] == root
|
|
assert calls["plan"][3] == 7300.0
|
|
assert calls["plan"][2][stop_validation.REENTRY_ENV] == "1"
|
|
|
|
observed = {}
|
|
payload = {"cwd": str(root), "stop_hook_active": False}
|
|
monkeypatch.setattr(stop_validation.sys, "stdin", io.StringIO(json.dumps(payload)))
|
|
monkeypatch.setattr(stop_validation, "is_document_only_turn", lambda *_: False)
|
|
monkeypatch.setattr(
|
|
stop_validation,
|
|
"_project_root",
|
|
lambda *args, **kwargs: observed.setdefault("root", kwargs["deadline"]) and root,
|
|
)
|
|
monkeypatch.setattr(
|
|
stop_validation,
|
|
"run_validations",
|
|
lambda *args, **kwargs: observed.setdefault("validation", kwargs["deadline"])
|
|
and (True, ""),
|
|
)
|
|
assert stop_validation.main(clock=clock) == 0
|
|
assert observed == {"root": 7300.0, "validation": 7300.0}
|
|
|
|
|
|
def test_command_defaults_and_shared_remaining_cap_are_preserved(tmp_path):
|
|
default_command = CommandSpec(("default",), tmp_path, "default")
|
|
assert default_command.timeout_seconds == 7200
|
|
plan = ValidationPlan(
|
|
ProjectKind.CMAKE,
|
|
(
|
|
ValidationStep(default_command),
|
|
ValidationStep(CommandSpec(("remaining",), tmp_path, "remaining")),
|
|
ValidationStep(CommandSpec(("explicit",), tmp_path, "explicit", 5)),
|
|
),
|
|
)
|
|
observed = []
|
|
|
|
def run(command, **kwargs):
|
|
observed.append(kwargs["timeout"])
|
|
return subprocess.CompletedProcess(command, 0, stdout="", stderr="")
|
|
|
|
execute_plan(
|
|
plan,
|
|
tmp_path,
|
|
run=run,
|
|
clock=TestClock((0.0, 0.0, 2000.0, 2000.0)),
|
|
)
|
|
assert observed == [7200, 5200.0, 5]
|
|
|
|
expired_plan = ValidationPlan(
|
|
ProjectKind.CMAKE,
|
|
(ValidationStep(default_command),),
|
|
)
|
|
with pytest.raises(ValidationFailure, match="timed out before it could start"):
|
|
execute_plan(expired_plan, tmp_path, deadline=0.0, clock=lambda: 0.0)
|
|
|
|
def nonzero(command, **kwargs):
|
|
return subprocess.CompletedProcess(command, 1, stdout="", stderr="failed")
|
|
|
|
with pytest.raises(ValidationFailure, match="failed with exit code 1"):
|
|
execute_plan(expired_plan, tmp_path, run=nonzero, clock=TestClock((0.0, 0.0)))
|
|
|
|
|
|
def test_registered_hook_and_one_time_invocation_contract(monkeypatch):
|
|
root = Path(__file__).resolve().parents[1]
|
|
hooks = json.loads((root / ".codex" / "hooks.json").read_text(encoding="utf-8"))
|
|
pre_tool = hooks["hooks"]["PreToolUse"][0]["hooks"][0]
|
|
stop = hooks["hooks"]["Stop"][0]["hooks"][0]
|
|
|
|
assert stop["timeout"] == 7500
|
|
assert pre_tool["timeout"] == 30
|
|
assert execute.StepExecutor.CODEX_TIMEOUT_SECONDS > stop["timeout"]
|
|
assert stop["timeout"] > stop_validation.TOTAL_TIMEOUT_SECONDS
|
|
assert all(
|
|
timeout > 0
|
|
for timeout in (
|
|
execute.StepExecutor.CODEX_TIMEOUT_SECONDS,
|
|
stop["timeout"],
|
|
stop_validation.TOTAL_TIMEOUT_SECONDS,
|
|
CommandSpec(("test",), root, "test").timeout_seconds,
|
|
)
|
|
)
|
|
|
|
main_calls = []
|
|
monkeypatch.setattr(execute.StepExecutor, "CODEX_TIMEOUT_SECONDS", 1800)
|
|
monkeypatch.setattr(
|
|
execute,
|
|
"main",
|
|
lambda: main_calls.append(execute.StepExecutor.CODEX_TIMEOUT_SECONDS),
|
|
)
|
|
exec(
|
|
"execute.StepExecutor.CODEX_TIMEOUT_SECONDS = 14400; execute.main()",
|
|
{"execute": execute},
|
|
)
|
|
assert main_calls == [14400]
|