From 6e00dba117d836fce02c6918abec06cbae9dbdeb Mon Sep 17 00:00:00 2001 From: "KOKO\\Mimi" Date: Sun, 6 Sep 2026 10:56:52 +0900 Subject: [PATCH] chore(mitc4-shell-feast-eigen-execution-prep): WIP blocked partial deadline preparation --- docs/HARNESS_WORKFLOW.md | 6 +- .../implementation-prep-evidence.md | 58 +++++ scripts/execute.py | 2 +- scripts/hooks/stop_validation.py | 2 +- scripts/msvc_harness/models.py | 2 +- scripts/msvc_harness/process.py | 2 +- tests/test_execute.py | 4 +- tests/test_harness_timeouts.py | 200 ++++++++++++++++++ 8 files changed, 268 insertions(+), 8 deletions(-) create mode 100644 docs/mitc4-shell-feast-eigen/implementation-prep-evidence.md create mode 100644 tests/test_harness_timeouts.py diff --git a/docs/HARNESS_WORKFLOW.md b/docs/HARNESS_WORKFLOW.md index 1569ba4..83fa7a5 100644 --- a/docs/HARNESS_WORKFLOW.md +++ b/docs/HARNESS_WORKFLOW.md @@ -523,8 +523,10 @@ Stop 검증이 실패한다. - 명령별 제한시간과 Stop 전체 제한시간 중 더 짧은 값을 적용한다. - 종료 코드가 0이 아니면 즉시 해당 stage를 실패 처리한다. -Stop 훅의 전체 제한시간은 저장소 탐색, toolchain 탐색, configure, build, test discovery, -test를 모두 포함해 1,800초다. `.codex/hooks.json`의 Stop command timeout도 1,800초다. +Stop 훅의 shared 전체 제한시간은 저장소 탐색, toolchain 탐색, configure, build, +test discovery, test를 모두 포함해 7,200초다. 각 command는 자체 제한시간과 남은 +shared deadline 중 더 짧은 값을 사용한다. `.codex/hooks.json`의 Stop command timeout은 +진단 여유를 포함해 7,500초다. 실패 메시지에는 다음 진단 정보를 포함한다. diff --git a/docs/mitc4-shell-feast-eigen/implementation-prep-evidence.md b/docs/mitc4-shell-feast-eigen/implementation-prep-evidence.md new file mode 100644 index 0000000..e84c7a5 --- /dev/null +++ b/docs/mitc4-shell-feast-eigen/implementation-prep-evidence.md @@ -0,0 +1,58 @@ +# Execution-prep evidence + +- Feature ID: `mitc4-shell-feast-eigen` +- Owner: `implementation-agent` +- Input contract revision: `r2` +- Step: `mitc4-shell-feast-eigen-execution-prep` / `0 bounded-execution-deadlines` +- Outcome: `blocked-environment` + +## RED + +`uv run --with pytest python -B -m pytest -v -rs tests/test_harness_timeouts.py` +returned exit code `1` twice. The first run exposed one test-double setup issue; after +that test-only correction, the second run failed `4/4` on the intended pre-change +defaults: Executor `1800`, Stop shared deadline `1800`, `CommandSpec` `1800`, and +registered Stop `1800`. + +## Applied scope and partial GREEN + +The following owned runtime defaults now match the approved finite limits: + +- `scripts/execute.py`: `StepExecutor.CODEX_TIMEOUT_SECONDS = 14400` +- `scripts/hooks/stop_validation.py`: `TOTAL_TIMEOUT_SECONDS = 7200` +- `scripts/msvc_harness/models.py`: `CommandSpec.timeout_seconds = 7200` +- `scripts/msvc_harness/process.py`: `execute_plan(... total_timeout_seconds=7200)` +- `docs/HARNESS_WORKFLOW.md`: shared `7200`-second deadline and `7500`-second + registered Stop explanation + +The next focused command returned exit code `1`: `2 passed, 2 failed`. The executor +and shared-deadline/process tests passed. The remaining hook contract test fails +because `.codex/hooks.json` still contains registered Stop timeout `1800`. + +## Blocker and unexecuted verification + +The assigned `apply_patch` edit for `.codex/hooks.json` was rejected twice by the +workspace sandbox as outside the writable project scope. The file is therefore +unchanged; no alternate write mechanism was used. This is an `environment` failure, +not a tolerance, contract, or implementation reinterpretation. + +Focused verification cannot reach exit `0` until the approved `1800 -> 7500` hook +edit is writable. Consequently the prescribed CMake configure/build/CTest and full +pytest verification were not run. No reference comparison is applicable to this +operational Step. The source currently loads an Executor wrapper deadline of `14400`, +but this session's actual registered Stop/Codex exit remains unobserved and the +registered config still declares `1800`. + +`git diff --check` completed with exit code `0`. A later read-only JSON status check +through `uv run python -B -c ...` could not initialize the user `uv` cache because +access to `C:\Users\baram\AppData\Local\uv\cache\sdists-v9\.git` was denied; it did +not modify the Step status and is additional environment evidence rather than a +replacement for the required verification. + +## Handoff + +Required environment action: make the owned repository file `.codex/hooks.json` +writable to this Step, then apply only its registered Stop timeout change to `7500` +and rerun Focused VERIFY followed by Full VERIFY. Existing `started_at` is preserved; +no `attempt_history`, timestamp, branch, commit, upstream contract, or reference +artifact was changed. diff --git a/scripts/execute.py b/scripts/execute.py index f4a33f7..5927894 100644 --- a/scripts/execute.py +++ b/scripts/execute.py @@ -76,7 +76,7 @@ class StepExecutor: """Phase 디렉토리 안의 step들을 순차 실행하는 하네스.""" MAX_TRANSPORT_ATTEMPTS = 2 - CODEX_TIMEOUT_SECONDS = 1800 + CODEX_TIMEOUT_SECONDS = 14400 CODEX_SANDBOX_ENV = "FEM_HARNESS_CODEX_SANDBOX" CODEX_SANDBOX_MODES = frozenset({"workspace-write", "danger-full-access"}) PHASE_NAME = re.compile(r"^[a-z0-9]+(?:[._-][a-z0-9]+)*$") diff --git a/scripts/hooks/stop_validation.py b/scripts/hooks/stop_validation.py index 7c3555f..4a0c76f 100644 --- a/scripts/hooks/stop_validation.py +++ b/scripts/hooks/stop_validation.py @@ -24,7 +24,7 @@ from hooks.validation_scope import is_document_only_turn REENTRY_ENV = "CODEX_STOP_VALIDATION_ACTIVE" -TOTAL_TIMEOUT_SECONDS = 1800 +TOTAL_TIMEOUT_SECONDS = 7200 def _remaining(deadline, clock, stage): diff --git a/scripts/msvc_harness/models.py b/scripts/msvc_harness/models.py index 1c03085..30fa0c6 100644 --- a/scripts/msvc_harness/models.py +++ b/scripts/msvc_harness/models.py @@ -71,7 +71,7 @@ class CommandSpec: argv: tuple[str, ...] cwd: Path stage: str - timeout_seconds: int = 1800 + timeout_seconds: int = 7200 @dataclass(frozen=True) diff --git a/scripts/msvc_harness/process.py b/scripts/msvc_harness/process.py index 0911b13..e671229 100644 --- a/scripts/msvc_harness/process.py +++ b/scripts/msvc_harness/process.py @@ -63,7 +63,7 @@ def execute_plan( root, *, env=None, - total_timeout_seconds=1800, + total_timeout_seconds=7200, deadline=None, run=subprocess.run, clock=time.monotonic, diff --git a/tests/test_execute.py b/tests/test_execute.py index d2a75e3..38ae64e 100644 --- a/tests/test_execute.py +++ b/tests/test_execute.py @@ -183,7 +183,7 @@ def test_invoke_codex_timeout_writes_structured_failure(tmp_path, monkeypatch): assert output["exitCode"] == 124 assert output["transportFailure"] == "timeout" assert output["stdout"] == "partial stdout" - assert "Codex timed out after 1800 seconds." in output["stderr"] + assert "Codex timed out after 14400 seconds." in output["stderr"] stored = json.loads((phase_dir / "step0-output.json").read_text(encoding="utf-8")) assert stored == output @@ -232,7 +232,7 @@ def test_execute_single_step_retries_structured_codex_timeout(tmp_path, monkeypa 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"] + assert "Codex timed out after 14400 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" diff --git a/tests/test_harness_timeouts.py b/tests/test_harness_timeouts.py new file mode 100644 index 0000000..2545583 --- /dev/null +++ b/tests/test_harness_timeouts.py @@ -0,0 +1,200 @@ +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]