fix(issue-17): a failed yes/no decision is not a "no" (review codex R1)
subscriber-up and stall-watch answered only by exit code, so a call that
could not run (module error rc 1, missing interpreter 127) read as "no":
with a subscriber that had ended 0, the error path mapped 0 to completed
and the submit ended with 0; a failed stall-watch silently skipped the
watcher.
Both now take --print: the answer is printed as yes/no and the exit code
is 0 for either. The wrapper asks with --print through _decided_yes, so a
nonzero exit, no answer or anything else is _decision_failed (ERROR line,
exit 1). Without --print the rc contract is unchanged (yes 0, no 1).
Tests: subscriber-up (direct and loop/discuss call sites) and stall-watch,
each failing by traceback, rc 127 or no answer with a subscriber that
ended 0, end the submit with 1 and the ERROR line - at 8241bd5 eight of the
nine ended with 0 (the loop/no-answer one ended with 1 through the loop's
own result and failed only on the ERROR line); yes and no keep their
branches in both paths; --print unit table; the static guard forbids an
rc-read yes/no decision.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCGpJXhSZasJLU38ksDghe
This commit is contained in:
1 parent
541c3febe8
commit
65b78b6ec4
3 files changed
+77
-13
No files matched your search
@@ -651,7 +651,7 @@ def _failing_py(tmp_path: Path, decision: str, mode: str) -> Path:
|
||||
return shim
|
||||
|
||||
|
||||
def _human_submit(tmp_path: Path, py: str) -> subprocess.CompletedProcess:
|
||||
def _human_submit(tmp_path: Path, py: str, typ: str = "direct", **extra) -> subprocess.CompletedProcess:
|
||||
"""The real wrapper, one human job, an unreachable broker: the subscriber
|
||||
ends with rc 3, the job is closed at its 3 s wall clock and the wrapper
|
||||
exits through sub-status, finalize-begin/-end and exit-code (rc 3)."""
|
||||
@@ -660,7 +660,10 @@ def _human_submit(tmp_path: Path, py: str) -> subprocess.CompletedProcess:
|
||||
env.update(HOME=str(tmp_path), WORKSPACE_ROOT=str(tmp_path), DELEGATE_JOB_PYTHON=py,
|
||||
AGENT_PYTHON_BIN=sys.executable, PYTHONPATH=str(SKILLS), DELEGATE_JOB_LOGS_DIR=str(tmp_path / "logs"),
|
||||
MAM_ENV_FILE=str(tmp_path / ".mam.env.absent"), MQTT_BROKER="127.0.0.1", MQTT_PORT="9", MQTT_TLS="0")
|
||||
return subprocess.run(["bash", str(WRAPPER), "submit", "--agent", "human", "--registry-dir", str(tmp_path / "reg"),
|
||||
env.update({k: str(v) for k, v in extra.items()})
|
||||
loop = ["--reviewer", "human", "--reviewer-session", "herdr:human"] if typ != "direct" else []
|
||||
return subprocess.run(["bash", str(WRAPPER), "submit", "--agent", "human", "--type", typ, *loop,
|
||||
"--registry-dir", str(tmp_path / "reg"),
|
||||
"--workdir", str(tmp_path), "--prompt", "n2", "--timeout", "3", "--idle-timeout", "2"],
|
||||
capture_output=True, text=True, env=env, cwd=str(tmp_path), timeout=120)
|
||||
|
||||
@@ -684,6 +687,40 @@ def test_f_a_failed_decision_ends_the_submit_with_1(tmp_path, decision, mode):
|
||||
assert f"ERROR: the wrapper could not decide '{decision}'" in res.stderr, res.stderr[-800:]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ["error", "gone", "empty"])
|
||||
@pytest.mark.parametrize("decision,typ", [("subscriber-up", "direct"), ("subscriber-up", "loop"),
|
||||
("stall-watch", "direct")])
|
||||
def test_f_a_failed_yes_no_decision_ends_the_submit_with_1(tmp_path, decision, typ, mode):
|
||||
"""Review R1 (codex): a yes/no decision that fails must not pass for "no".
|
||||
The subscriber stand-in ends with 0 before SUBSCRIBED: read as "not up",
|
||||
the error path mapped rc 0 to completed and the submit ended with 0."""
|
||||
res = _human_submit(tmp_path, str(_failing_py(tmp_path, decision, mode)), typ, SUB_STANDIN=1, SUB_RC=0)
|
||||
assert res.returncode == 1, (res.returncode, res.stderr[-800:])
|
||||
assert f"ERROR: the wrapper could not decide '{decision}'" in res.stderr, res.stderr[-800:]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("typ", ["direct", "loop"])
|
||||
@pytest.mark.parametrize("sub_rc,up", [(0, True), (1, False)])
|
||||
def test_f_yes_and_no_keep_their_branches(tmp_path, typ, sub_rc, up):
|
||||
"""The same runs without a failure: "yes" goes on, "no" takes the error path."""
|
||||
res = _human_submit(tmp_path, str(_failing_py(tmp_path, "none", "error")), typ, SUB_STANDIN=1, SUB_RC=sub_rc)
|
||||
assert "could not decide" not in res.stderr, res.stderr[-800:]
|
||||
assert ("subscriber died early" in res.stderr) is (not up), res.stderr[-800:]
|
||||
if typ == "direct":
|
||||
assert res.returncode == (0 if up else 1), (res.returncode, res.stderr[-800:])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("rc,up", [(0, True), (3, True), (1, False), (2, False), (4, False)])
|
||||
def test_u_yes_no_print(capsys, rc, up):
|
||||
assert decide.main(["subscriber-up", str(rc), "--print"]) == 0
|
||||
assert capsys.readouterr().out == ("yes\n" if up else "no\n")
|
||||
assert decide.main(["subscriber-up", str(rc)]) == (0 if up else 1) # the rc form is unchanged
|
||||
assert decide.main(["stall-watch", "herdr:c", "claude", "--print"]) == 0
|
||||
assert capsys.readouterr().out == "yes\n"
|
||||
assert decide.main(["stall-watch", "remote:r", "claude", "--print"]) == 0
|
||||
assert capsys.readouterr().out == "no\n"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ["error", "empty"])
|
||||
def test_f_a_failed_wait_rc_ends_wait_with_1(tmp_path, mode):
|
||||
py = _failing_py(tmp_path, "wait-rc", mode)
|
||||
@@ -695,11 +732,12 @@ def test_f_a_failed_wait_rc_ends_wait_with_1(tmp_path, mode):
|
||||
|
||||
|
||||
def test_s_no_printed_decision_is_read_raw():
|
||||
"""Every printed answer goes through _decided/_exit_for; the boolean ones
|
||||
(subscriber-up, stall-watch) are read by their rc, and finalize-end is
|
||||
checked by its rc."""
|
||||
"""Every printed answer goes through _decided/_exit_for/_decided_yes (the
|
||||
yes/no ones with --print), and finalize-end is checked by its rc."""
|
||||
text = "\n".join(l for l in WRAPPER.read_text().splitlines() if not l.lstrip().startswith("#"))
|
||||
raw = [l.strip() for l in text.splitlines() if "$(_decide" in l]
|
||||
assert raw == ['__out="$(_decide "$@")" || __rc=$?',
|
||||
'{ out="$(_decide finalize-end --job "$job_id" --registry-dir "$REGISTRY_DIR" --status "$job_status" \\'], raw
|
||||
assert 'exit "$(' not in text
|
||||
assert not re.search(r"\b_decide (subscriber-up|stall-watch)\b", text), "a yes/no decision read by its rc"
|
||||
assert text.count("_decided_yes subscriber-up") == 2 and text.count("_decided_yes stall-watch") == 1
|
||||
Reference in new issue
Block a user