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:
1 parent
8adbe2245c
commit
12cba25c18
7 files changed
+47
-14
No files matched your search
+12
-1
@@ -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
|
||||
@@ -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(
|
||||
|
||||
@@ -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"])
|
||||
|
||||
Reference in new issue
Block a user