fix(issue-33): subscriber exit codes are named on both sides; wait keeps a spent budget at rc 2; G-8 is deterministic

- job_subscriber.py returns only named codes (RC_COMPLETED 0, RC_ERROR 1,
  RC_NO_EVENT 2, RC_BROKER_UNAVAILABLE 3, RC_WALL_CLOCK 4). The wrapper
  mirrors them as SUB_RC_* and compares subscriber codes only by name; a test
  keeps the mirror equal to the Python constants and forbids numeric
  comparisons of $sub_rc/$sub_exit and numeric `return` in the subscriber.
- `delegate-job wait` passed the subscriber's code straight through, so a
  spent budget would have reached its callers as 4; it is 2 again (B7 fails
  without the mapping).
- tier1 G-8 gave both clocks 0.5 s and raced; the wall clock now runs out
  first by construction (idle 30 s) and the test asserts RC_WALL_CLOCK.
  Other subscriber rc asserts use the names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
GodopuandClaude Opus 5.5 committed 2026-09-26 00:22:43 +00:00
1 parent 9ec0569401
commit 1673818bb5
6 files changed
+77 -37

No files matched your search

+1 -1
View File
@@ -1030,7 +1030,7 @@ def test_a_replayed_retained_state_does_not_keep_a_silent_job_alive(tmp_path, mo
"--timeout", "20"])
elapsed = time.monotonic() - started
fake.stopped.set()
assert rc == 2
assert rc == js.RC_NO_EVENT
assert elapsed < 8, f"the replayed state kept the silent job alive for {elapsed:.1f}s"
+15
View File
@@ -341,3 +341,18 @@ def test_r3_a_budget_spent_waiting_on_an_unreachable_broker_is_wall_clock_timeou
if p.poll() is None:
p.kill()
p.communicate(timeout=10)
# Issue #33 follow-up (job 60da22f6): `delegate-job wait` passes the subscriber's
# code through; a spent budget stays rc 2 for its callers, as before the split.
@pytest.mark.parametrize("state,rc", [("running", 2), ("completed", 0)])
def test_b7_wait_keeps_a_spent_budget_at_rc_2(tmp_path, state, rc):
env = _env(tmp_path, broker=True)
reg = tmp_path / "reg"
jid = _jid(_dry_submit(env, reg))
if state == "completed":
_publish(env, reg, jid, "completed")
res = subprocess.run(["bash", str(WRAPPER), "wait", "--job", jid, "--registry-dir", str(reg), "--timeout", "2"],
capture_output=True, text=True, env=env, cwd=env["HOME"], timeout=60)
assert res.returncode == rc, res.stdout + res.stderr
+17 -5
View File
@@ -361,7 +361,11 @@ def test_job_subscriber_cli_contract_snapshot(mam_sandbox):
for doc in (" 0 all watched jobs reached", " 1 any watched job reached",
" 2 no event for the idle window", " 4 the wall-clock budget"):
assert doc in src, doc
assert 'return 3' in src, "fatal-error exit code 3 must survive"
# Issue #33: codes are named; their values are the frozen contract.
assert (job_subscriber.RC_COMPLETED, job_subscriber.RC_ERROR, job_subscriber.RC_NO_EVENT,
job_subscriber.RC_BROKER_UNAVAILABLE, job_subscriber.RC_WALL_CLOCK) == (0, 1, 2, 3, 4)
assert 'return RC_BROKER_UNAVAILABLE' in src, "fatal-error exit code 3 must survive"
assert not re.search(r"^\s+return [0-9]+\s*(#.*)?$", src, re.M), "exit codes are returned by name"
# ==============================================================================
@@ -378,17 +382,25 @@ def test_event_to_status_mapping_is_frozen(mam_sandbox):
}
def test_wrapper_exit_code_mapping_literals_are_frozen():
def test_wrapper_exit_code_mapping_literals_are_frozen(mam_sandbox):
wrapper = SKILLS / "multi-agent-mux-delegate-job" / "multi-agent-mux-delegate-job"
text = wrapper.read_text(encoding="utf-8")
for literal in (
'if [[ $sub_rc -eq 0 ]]; then',
'elif [[ $sub_rc -eq 1 ]]; then',
'elif [[ $sub_rc -eq 3 ]]; then',
'if [[ $sub_rc -eq $SUB_RC_COMPLETED ]]; then',
'elif [[ $sub_rc -eq $SUB_RC_ERROR ]]; then',
'elif [[ $sub_rc -eq $SUB_RC_BROKER_UNAVAILABLE ]]; then',
'[ "$sub_rc" = "$SUB_RC_WALL_CLOCK" ] && clock="wall"',
'job_status="completed"',
'job_status="error"',
):
assert literal in text, literal
# Issue #33: the wrapper's SUB_RC_* mirror job_subscriber's RC_* exactly,
# and the wrapper compares subscriber codes only by those names.
job_subscriber = _import_delegation(mam_sandbox, "job_subscriber")
mirror = dict(re.findall(r"\bSUB_RC_([A-Z_]+)=([0-9]+)", text))
assert mirror == {name[3:]: str(getattr(job_subscriber, name))
for name in dir(job_subscriber) if name.startswith("RC_")}, mirror
assert not re.search(r"\$(sub_rc|sub_exit)\"? +(-eq|-ne|=) +\"?[0-9]", text), "subscriber codes compared by value"
def test_reconcile_terminal_mutation_literals_are_frozen():
+9 -7
View File
@@ -721,7 +721,7 @@ def test_g5_subscriber_disk_fallback_on_broker_down_exits_0(mam_sandbox, monkeyp
"--timeout", "5",
"--idle-timeout", "2"
])
assert rc == 0
assert rc == job_subscriber.RC_COMPLETED
def test_g6_subscriber_disk_fallback_outputs_source_tag(mam_sandbox, capsys, monkeypatch):
@@ -744,7 +744,7 @@ def test_g6_subscriber_disk_fallback_outputs_source_tag(mam_sandbox, capsys, mon
"--job", "g6test01",
"--timeout", "5"
])
assert rc == 0
assert rc == job_subscriber.RC_COMPLETED
captured = capsys.readouterr()
assert "disk-fallback" in captured.out
@@ -769,7 +769,7 @@ def test_g7_subscriber_disk_fallback_error_status_exits_1(mam_sandbox, monkeypat
"--job", "g7test01",
"--timeout", "5"
])
assert rc == 1
assert rc == job_subscriber.RC_ERROR
def test_g8_subscriber_wait_any_does_not_exit_early_if_partial_pending(mam_sandbox, monkeypatch):
@@ -806,13 +806,15 @@ def test_g8_subscriber_wait_any_does_not_exit_early_if_partial_pending(mam_sandb
monkeypatch.setattr(job_subscriber, "make_client", lambda *args, **kwargs: FakeClient())
monkeypatch.setattr(mqtt_common, "with_retry", lambda fn, **kwargs: fn)
# The wall clock (0.5 s) runs out well before the idle window (30 s), so
# which timeout ends the wait is decided, not a race (Issue #33 rc split).
rc = job_subscriber.main([
"--registry-dir", reg_dir,
"--wait-any",
"--timeout", "0.5",
"--idle-timeout", "0.5"
"--idle-timeout", "30"
])
assert rc == 2, "Must timeout waiting for incomplete job2 rather than exiting 0"
assert rc == job_subscriber.RC_WALL_CLOCK, "Must timeout waiting for incomplete job2 rather than exiting 0"
def test_g9_subscriber_broker_down_no_disk_terminal_exits_3(mam_sandbox, monkeypatch):
@@ -835,14 +837,14 @@ def test_g9_subscriber_broker_down_no_disk_terminal_exits_3(mam_sandbox, monkeyp
"--job", "g9test01",
"--timeout", "5"
])
assert rc == 3, f"Expected rc=3 on infrastructure broker down without disk terminal, got {rc}"
assert rc == job_subscriber.RC_BROKER_UNAVAILABLE, f"Expected rc=3 on infrastructure broker down without disk terminal, got {rc}"
def test_g10_delegate_job_rc3_not_mistaken_for_error(mam_sandbox):
"""G-10: multi-agent-mux-delegate-job maps rc=3 to broker_unavailable and checks disk status."""
delegate_script = mam_sandbox / "skills" / "multi-agent-mux-delegate-job" / "multi-agent-mux-delegate-job"
content = delegate_script.read_text()
assert "elif [[ $sub_rc -eq 3 ]]; then" in content
assert "elif [[ $sub_rc -eq $SUB_RC_BROKER_UNAVAILABLE ]]; then" in content
assert 'job_status="broker_unavailable"' in content