fix(issue-5): keep finalize inside delegation and refresh the CLI golden

Two failures the full suite caught, both mine.

finalize.py started in lib_py/loop, which made publish_event import a sibling
package. That is the rule phase 1 wrote down and phase 2a already made me move
the engine for: under a frozen loop, delegation is bound to one tree while the
parent may be another, so reaching sideways mixes them. The manifest is written
into the job's own report directory and the CLI that declares it is a delegation
entry point, so finalize belongs there. Second time on the same rule, and the
guard caught it both times.

The publish_event --help golden no longer matched, because phase 2b
intentionally added the finalization options. The diff is exactly those three
options and nothing else, so the capture is refreshed rather than the assertion
weakened, and the test now states when a refresh is legitimate: an intended
extension, never drift nobody meant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrDNJkHmm18uu7n7JwV5Qd
This commit is contained in:
GodopuandClaude Opus 5 committed 2026-09-06 21:47:54 +09:00
1 parent 8adbe2245c
commit 12cba25c18
7 files changed
+47 -14

No files matched your search

+12 -1
View File
@@ -3,7 +3,9 @@ usage: publish_event.py [-h] (--job JOB | --pick-pending)
[--event {started,permission_required,progress,completed,error,cancelled}]
[--detail DETAIL] [--data DATA] [--retained]
[--registry-dir REGISTRY_DIR] [--attempts ATTEMPTS]
[-v]
[--finalize-report PATH]
[--side-effect KIND=REFERENCE]
[--attempt-id ATTEMPT_ID] [-v]
Publish a Job event to MQTT
@@ -19,4 +21,13 @@ optional arguments:
--retained force retain=True (auto for completed/error)
--registry-dir REGISTRY_DIR
--attempts ATTEMPTS
--finalize-report PATH
atomically finalize this report and declare its
completion manifest before publishing
--side-effect KIND=REFERENCE
evidence for an external side effect this job
performed
--attempt-id ATTEMPT_ID
attempt this finalization belongs to (default: the
job's current iteration)
-v, --verbose
+7 -1
View File
@@ -488,7 +488,13 @@ def test_importing_delegation_does_not_mutate_environment(mam_sandbox):
def test_shim_help_output_matches_pre_move_capture():
"""The shim must be indistinguishable from the pre-move script at the CLI."""
"""The shim must be indistinguishable from the implementation at the CLI.
The captures were taken before the phase 1 move to prove the move changed
nothing. They are refreshed only when the CLI is *intentionally* extended
(phase 2b added the finalization options to publish_event); the guard's job
is to catch drift nobody meant, which it still does.
"""
for mod in ("publish_event", "registry", "job_subscriber"):
golden = (FIXTURES / "help-{}.txt".format(mod)).read_text(encoding="utf-8")
res = subprocess.run(
+12 -10
View File
@@ -154,7 +154,7 @@ def _report(tmp_path, body="done\n", staged=False):
def test_finalize_promotes_a_staged_report_atomically(tmp_path):
"""A reader must never see a half-written report as finalized."""
from lib_py.loop import finalize
from lib_py.delegation import finalize
path = _report(tmp_path, "final body\n", staged=True)
assert not path.exists() and path.with_name(path.name + ".tmp").exists()
@@ -165,7 +165,7 @@ def test_finalize_promotes_a_staged_report_atomically(tmp_path):
def test_finalize_verifies_against_the_file_not_the_declaration(tmp_path):
"""The hash is recomputed, so a forged manifest cannot certify itself."""
from lib_py.loop import finalize
from lib_py.delegation import finalize
path = _report(tmp_path)
finalize.finalize_report(path, "job1", 1)
@@ -181,7 +181,7 @@ def test_finalize_verifies_against_the_file_not_the_declaration(tmp_path):
def test_finalize_is_invalidated_by_a_rewritten_report(tmp_path):
"""A new body needs a new finalization; the old hash is void immediately."""
from lib_py.loop import finalize
from lib_py.delegation import finalize
path = _report(tmp_path, "first\n")
finalize.finalize_report(path, "job1", 1)
@@ -196,7 +196,7 @@ def test_finalize_is_invalidated_by_a_rewritten_report(tmp_path):
({"expected_attempt": 9}, "belongs to attempt"),
])
def test_finalize_rejects_a_manifest_for_a_different_job_or_attempt(tmp_path, kwargs, fragment):
from lib_py.loop import finalize
from lib_py.delegation import finalize
path = _report(tmp_path)
finalize.finalize_report(path, "job1", 1)
args = {"job_id": "job1"}
@@ -207,7 +207,7 @@ def test_finalize_rejects_a_manifest_for_a_different_job_or_attempt(tmp_path, kw
def test_finalize_requires_evidence_for_every_declared_side_effect(tmp_path):
"""Partial evidence is no evidence: the work is not known to be complete."""
from lib_py.loop import finalize
from lib_py.delegation import finalize
path = _report(tmp_path)
finalize.finalize_report(path, "job1", 1,
@@ -220,7 +220,7 @@ def test_finalize_requires_evidence_for_every_declared_side_effect(tmp_path):
def test_finalize_rejects_a_malformed_side_effect_pair():
from lib_py.loop import finalize
from lib_py.delegation import finalize
with pytest.raises(ValueError):
finalize.parse_side_effects(["nokey"])
with pytest.raises(ValueError):
@@ -228,7 +228,7 @@ def test_finalize_rejects_a_malformed_side_effect_pair():
def test_finalize_missing_manifest_is_a_refusal_not_a_crash(tmp_path):
from lib_py.loop import finalize
from lib_py.delegation import finalize
d = tmp_path / "empty"
d.mkdir()
verdict = finalize.verify_manifest(d, "job1")
@@ -239,7 +239,7 @@ def test_publish_event_finalizes_before_it_publishes(mam_sandbox, tmp_path, monk
"""The CLI surface an agent actually calls."""
monkeypatch.chdir(mam_sandbox)
from lib_py.delegation import publish_event
from lib_py.loop import finalize
from lib_py.delegation import finalize
reg = _reg(tmp_path)
job_id = _make_job(reg)
@@ -280,7 +280,8 @@ def test_publish_event_reports_a_bad_side_effect_instead_of_publishing(mam_sandb
def _ready_job(tmp_path, mam_sandbox, *, backend="claude-code", opt_in=True,
artifacts=None, body="report\n"):
"""A job in exactly the state that should auto-complete, plus its evidence."""
from lib_py.loop import finalize, store
from lib_py.delegation import finalize
from lib_py.loop import store
reg = _reg(tmp_path)
job_id = _make_job(reg, agent=backend, expected_artifacts=artifacts or [])
@@ -398,7 +399,8 @@ def test_g5_a_rewritten_report_stops_the_gate(mam_sandbox, tmp_path, monkeypatch
def test_g5_missing_side_effect_evidence_stops_the_gate(mam_sandbox, tmp_path, monkeypatch):
monkeypatch.chdir(mam_sandbox)
monkeypatch.setenv("MAM_AUTO_COMPLETE", "1")
from lib_py.loop import autocomplete, finalize
from lib_py.loop import autocomplete
from lib_py.delegation import finalize
job_id, reg, scenario, report_dir = _ready_job(tmp_path, mam_sandbox,
artifacts=["git_push"])