"""Hub decision write-back behind INFD_HUB_WRITEBACK (INFD-WP-0005-T03). Offline.""" import pytest from informed_decision.hub_writeback import Answer, build_resolution, write_back D1 = "62aa95e4-3ccc-4867-8806-5e3db005b483" BASE = "http://hub.test" ON = {"INFD_HUB_WRITEBACK": "1"} def answer(**kw): data = dict(memo_id="infd-20261003-p01", memo_version=1, verb="accept", disposition_id="disp-1", decided_at="2026-10-03T12:00:00Z", reviewer_display="Bernd Worsch", approval_id="ba5ce2d8-8b6d-40be-af89-2e8c147029a3", approval_binding_digest="sha256:" + "a" * 64, view_hash="sha256:" + "b" * 64) data.update(kw) return Answer(**data) class Hub: def __init__(self, *responses): self.responses = list(responses) self.calls = [] def __call__(self, method, url, body): self.calls.append((method, url, body)) return self.responses.pop(0) def test_rationale_carries_memo_id_and_pins(): payload = build_resolution(answer()) assert "memo infd-20261003-p01 v1" in payload["rationale"] assert "approval ba5ce2d8" in payload["rationale"] assert payload["decided_by"] == "Bernd Worsch via informed-decision" def test_agents_cannot_resolve(): with pytest.raises(ValueError): build_resolution(answer(actor_kind="agent")) def test_flag_off_builds_but_does_not_send(): hub = Hub() out = write_back(answer(), {"hub_decision_id": D1, "resolves": "on-answer"}, base_url=BASE, transport=hub, env={}) assert out.status == "skipped" and out.payload and hub.calls == [] @pytest.mark.parametrize("verb,serves", [ ("return", {"hub_decision_id": D1, "resolves": "on-answer"}), ("discuss", {"hub_decision_id": D1, "resolves": "on-answer"}), ("accept", {"hub_decision_id": D1, "resolves": "never"}), ("accept", None), ]) def test_not_applicable(verb, serves): hub = Hub() out = write_back(answer(verb=verb), serves, base_url=BASE, transport=hub, env=ON) assert out.status == "not_applicable" and hub.calls == [] def test_resolves_when_on(): hub = Hub((200, {"id": D1, "status": "resolved"})) out = write_back(answer(verb="decline"), {"hub_decision_id": D1, "resolves": "on-answer"}, base_url=BASE, transport=hub, env=ON) assert out.status == "resolved" assert hub.calls[0][:2] == ("POST", f"{BASE}/decisions/{D1}/resolve") def test_409_is_idempotent_only_for_our_memo(): ours = Hub((409, {}), (200, {"rationale": "accept in informed-decision memo infd-20261003-p01 v1; ..."})) assert write_back(answer(), {"hub_decision_id": D1, "resolves": "on-answer"}, base_url=BASE, transport=ours, env=ON).status == "already_resolved" theirs = Hub((409, {}), (200, {"rationale": "resolved by hand"})) assert write_back(answer(), {"hub_decision_id": D1, "resolves": "on-answer"}, base_url=BASE, transport=theirs, env=ON).status == "conflict" def test_transport_error_is_failed_not_raised(): def broken(*_): raise ConnectionRefusedError() out = write_back(answer(), {"hub_decision_id": D1, "resolves": "on-answer"}, base_url=BASE, transport=broken, env=ON) assert out.status == "failed" def test_bad_decision_id_refused(): with pytest.raises(ValueError): write_back(answer(), {"hub_decision_id": "D1", "resolves": "on-answer"}, base_url=BASE, transport=Hub(), env=ON) @pytest.mark.parametrize('read_status,body', [(503, {}), (404, {}), (200, {}), (200, None)]) def test_unreadable_conflict_remains_retryable(read_status, body): hub = Hub((409, {}), (read_status, body)) assert write_back(answer(), {'hub_decision_id': D1, 'resolves': 'on-answer'}, base_url=BASE, transport=hub, env=ON).status == 'failed'