"""Model-free tests for the MAIN-to-RX acknowledgement channel.""" from __future__ import annotations from dataclasses import replace from datetime import datetime, timedelta, timezone from pathlib import Path import subprocess import pytest import tools.rx_agent_watcher as rx_agent_watcher from tools.rf_handoff_channel import ( CHANNEL_TEST_TASK_ID, COMPLETE_STATE, GitResultChannel, READY_STATE, ResultBranchMissing, ResultChannelError, ResultHistoryRewritten, RxHandoffResult, result_digest, ) from tools.tx_agent_watcher import TxWatcherState, process_history_cycle from tools.tx_handoff_channel import ( CHANNEL_COMPLETE_OBSERVED, CHANNEL_READY_OBSERVED, TX_ACK_SCHEMA_VERSION, TX_AUTHORIZED, TX_COMPLETE, TX_STARTED, TX_RESULT_PATHS, GitAcknowledgementChannel, TxHandoffAcknowledgement, acknowledgement_from_mapping, build_tx_execution_event, channel_test_acknowledgements, ) NOW = datetime(2026, 9, 22, 12, 0, tzinfo=timezone.utc) SOURCE_HEAD = "a" * 40 def _git(arguments: list[str], cwd: Path) -> str: completed = subprocess.run( ["git", *arguments], cwd=cwd, capture_output=True, text=True, encoding="utf-8", errors="replace", check=True, ) return completed.stdout.strip() def repository_fixture( tmp_path: Path, *, with_tx_node: bool = True, ) -> tuple[Path, Path, str | None]: remote = tmp_path / "remote.git" work = tmp_path / "work" _git(["init", "--bare", str(remote)], tmp_path) work.mkdir() _git(["init"], work) _git(["config", "user.name", "Test"], work) _git(["config", "user.email", "test@example.invalid"], work) (work / "README.md").write_text("fixture\n", encoding="utf-8") _git(["add", "README.md"], work) _git(["commit", "-m", "Seed RX result branch"], work) initial_branch = _git(["branch", "--show-current"], work) _git(["remote", "add", "origin", str(remote)], work) _git(["push", "origin", "HEAD:refs/heads/rx-node"], work) tx_root: str | None = None if with_tx_node: _git(["switch", "--orphan", "tx-node"], work) _git(["rm", "-rf", "--ignore-unmatch", "."], work) _git(["commit", "--allow-empty", "-m", "Initialize tx-node"], work) tx_root = _git(["rev-parse", "HEAD"], work) _git(["push", "origin", "HEAD:refs/heads/tx-node"], work) _git(["switch", initial_branch], work) return remote, work, tx_root def channel_ready(**changes: object) -> RxHandoffResult: result = RxHandoffResult( schema_version=1, task_id=CHANNEL_TEST_TASK_ID, state=READY_STATE, rx_ready_for_tx=True, process_alive=True, file_growing=True, capture_pid=1234, published_at=NOW.isoformat(), capture_started_at=NOW.isoformat(), expected_capture_end=(NOW + timedelta(seconds=60)).isoformat(), capture_path="MODEL_FREE_FAKE_CAPTURE", common_clock_verified=False, source_head=SOURCE_HEAD, rf_tx=0, exit_code=None, file_size=None, duration_seconds=None, overruns=None, usb_errors=None, file_errors=None, sha256=None, clkin_after="NOT_RUN", rf_tx_observed="UNKNOWN", ) return replace(result, **changes) def channel_complete( ready: RxHandoffResult, **changes: object, ) -> RxHandoffResult: result = replace( ready, state=COMPLETE_STATE, rx_ready_for_tx=False, process_alive=False, file_growing=False, published_at=(NOW + timedelta(seconds=1)).isoformat(), exit_code=0, file_size=4, duration_seconds=1.0, overruns=0, usb_errors=0, file_errors=0, sha256="b" * 64, ) return replace(result, **changes) def acknowledgement( state: str, source_commit: str, source_digest: str, **changes: object, ) -> TxHandoffAcknowledgement: result = TxHandoffAcknowledgement( schema_version=1, task_id=CHANNEL_TEST_TASK_ID, state=state, source_rx_commit=source_commit, source_rx_digest=source_digest, source_head=SOURCE_HEAD, observed_at=NOW.isoformat(), main_watcher_pid=4321, rf_tx_authorized=False, rf_tx_started=False, rf_tx=0, ) return replace(result, **changes) class FakeExecutor: def __init__(self) -> None: self.calls = 0 def prepare(self, *_args: object, **_kwargs: object) -> Path: self.calls += 1 raise AssertionError("channel test must not prepare RF TX") def test_acknowledgement_schema_binds_digest_and_source_commit() -> None: valid = acknowledgement(CHANNEL_READY_OBSERVED, "1" * 40, "2" * 64) raw = valid.__dict__.copy() assert acknowledgement_from_mapping(raw) == valid with pytest.raises(ValueError, match="source_rx_commit"): acknowledgement_from_mapping({**raw, "source_rx_commit": "1" * 39}) with pytest.raises(ValueError, match="source_rx_digest"): acknowledgement_from_mapping({**raw, "source_rx_digest": "2" * 63}) with pytest.raises(ValueError, match="cannot claim RF TX"): acknowledgement_from_mapping({**raw, "rf_tx_authorized": True}) def test_tx_events_preserve_authorization_binding_and_timestamps() -> None: authorization = TxHandoffAcknowledgement( schema_version=TX_ACK_SCHEMA_VERSION, task_id="RF001A_OTA_CAPTURE_004", state=TX_AUTHORIZED, source_rx_commit="1" * 40, source_rx_digest="2" * 64, source_head=SOURCE_HEAD, observed_at=NOW.isoformat(), main_watcher_pid=4321, rf_tx_authorized=True, rf_tx_started=False, rf_tx=0, tx_task_id="RF001A_OTA_TX_004", ) started_at = (NOW + timedelta(seconds=1)).isoformat() started = build_tx_execution_event( authorization, state=TX_STARTED, observed_at=started_at, tx_started_at=started_at, ) finished_at = (NOW + timedelta(seconds=2.2)).isoformat() complete = build_tx_execution_event( authorization, state=TX_COMPLETE, observed_at=finished_at, tx_started_at=started_at, tx_finished_at=finished_at, process_exit_code=0, underruns=0, errors=0, ) assert started.source_rx_commit == authorization.source_rx_commit assert started.source_rx_digest == authorization.source_rx_digest assert started.rf_tx_started and started.rf_tx == 1 assert complete.tx_finished_at == finished_at assert complete.tx_process_exit_code == 0 def test_execution_event_publication_requires_authorization_and_start( tmp_path: Path, ) -> None: _remote, work, _tx_root = repository_fixture(tmp_path) channel = GitAcknowledgementChannel(work) authorization = TxHandoffAcknowledgement( schema_version=TX_ACK_SCHEMA_VERSION, task_id="RF001A_OTA_CAPTURE_004", state=TX_AUTHORIZED, source_rx_commit="1" * 40, source_rx_digest="2" * 64, source_head=SOURCE_HEAD, observed_at=NOW.isoformat(), main_watcher_pid=4321, rf_tx_authorized=True, rf_tx_started=False, rf_tx=0, tx_task_id="RF001A_OTA_TX_004", ) started_at = (NOW + timedelta(seconds=1)).isoformat() finished_at = (NOW + timedelta(seconds=2.2)).isoformat() started = build_tx_execution_event( authorization, state=TX_STARTED, observed_at=started_at, tx_started_at=started_at, ) complete = build_tx_execution_event( authorization, state=TX_COMPLETE, observed_at=finished_at, tx_started_at=started_at, tx_finished_at=finished_at, process_exit_code=0, underruns=0, errors=0, ) with pytest.raises(ResultChannelError, match="TX_AUTHORIZED"): channel.publish_execution_event(started) channel.publish(authorization) with pytest.raises(ResultChannelError, match="TX_STARTED"): channel.publish_execution_event(complete) started_outcome = channel.publish_execution_event(started) complete_outcome = channel.publish_execution_event(complete) assert started_outcome.status == "PUBLISHED" assert complete_outcome.status == "PUBLISHED" def test_main_publish_uses_exact_allowlist_and_suppresses_duplicate( tmp_path: Path, ) -> None: remote, work, tx_root = repository_fixture(tmp_path) channel = GitAcknowledgementChannel(work) item = acknowledgement(CHANNEL_READY_OBSERVED, "1" * 40, "2" * 64) first = channel.publish(item) second = channel.publish(item) assert first.status == "PUBLISHED" assert second.status == "UNCHANGED" assert second.commit == first.commit assert tx_root is not None assert _git( ["--git-dir", str(remote), "rev-list", "--count", "refs/heads/tx-node"], tmp_path, ) == "2" tree = set( _git( [ "--git-dir", str(remote), "ls-tree", "-r", "--name-only", "refs/heads/tx-node", ], tmp_path, ).splitlines() ) assert tree == TX_RESULT_PATHS def test_two_acknowledgements_between_polls_are_both_confirmed( tmp_path: Path, ) -> None: _remote, work, _tx_root = repository_fixture(tmp_path) channel = GitAcknowledgementChannel(work) ready_commit = "1" * 40 ready_digest = "2" * 64 complete_commit = "3" * 40 complete_digest = "4" * 64 ready_outcome = channel.publish( acknowledgement(CHANNEL_READY_OBSERVED, ready_commit, ready_digest) ) complete_outcome = channel.publish( acknowledgement( CHANNEL_COMPLETE_OBSERVED, complete_commit, complete_digest, observed_at=(NOW + timedelta(seconds=1)).isoformat(), ) ) status = channel_test_acknowledgements( channel, expected_source_head=SOURCE_HEAD, ready_source_commit=ready_commit, ready_source_digest=ready_digest, complete_source_commit=complete_commit, complete_source_digest=complete_digest, ) assert status.ready_confirmed assert status.complete_confirmed assert status.ready_ack_commit == ready_outcome.commit assert status.complete_ack_commit == complete_outcome.commit @pytest.mark.parametrize("wrong_field", ["digest", "commit"]) def test_wrong_binding_does_not_confirm_ready( tmp_path: Path, wrong_field: str, ) -> None: _remote, work, _tx_root = repository_fixture(tmp_path) channel = GitAcknowledgementChannel(work) expected_commit = "1" * 40 expected_digest = "2" * 64 channel.publish( acknowledgement( CHANNEL_READY_OBSERVED, "9" * 40 if wrong_field == "commit" else expected_commit, "8" * 64 if wrong_field == "digest" else expected_digest, ) ) status = channel_test_acknowledgements( channel, expected_source_head=SOURCE_HEAD, ready_source_commit=expected_commit, ready_source_digest=expected_digest, complete_source_commit="3" * 40, complete_source_digest="4" * 64, ) assert not status.ready_confirmed assert not status.complete_confirmed def test_stale_acknowledgement_with_old_source_head_is_rejected( tmp_path: Path, ) -> None: _remote, work, _tx_root = repository_fixture(tmp_path) channel = GitAcknowledgementChannel(work) channel.publish( acknowledgement( CHANNEL_READY_OBSERVED, "1" * 40, "2" * 64, source_head="f" * 40, ) ) status = channel_test_acknowledgements( channel, expected_source_head=SOURCE_HEAD, ready_source_commit="1" * 40, ready_source_digest="2" * 64, complete_source_commit="3" * 40, complete_source_digest="4" * 64, ) assert not status.ready_confirmed def test_missing_tx_node_is_blocked(tmp_path: Path) -> None: _remote, work, _tx_root = repository_fixture(tmp_path, with_tx_node=False) channel = GitAcknowledgementChannel(work) with pytest.raises(ResultBranchMissing): channel_test_acknowledgements( channel, expected_source_head=SOURCE_HEAD, ) def test_acknowledgement_push_failure_preserves_remote_head( tmp_path: Path, ) -> None: _remote, work, _tx_root = repository_fixture(tmp_path) class FailingChannel(GitAcknowledgementChannel): def _run( self, arguments: list[str], *, cwd: Path | None = None, check: bool = True, ) -> subprocess.CompletedProcess[str]: if arguments and arguments[0] == "push": return subprocess.CompletedProcess( ["git", *arguments], 1, "", "simulated push failure", ) return super()._run(arguments, cwd=cwd, check=check) channel = FailingChannel(work) before = channel.remote_head() with pytest.raises(ResultChannelError, match="simulated push failure"): channel.publish( acknowledgement(CHANNEL_READY_OBSERVED, "1" * 40, "2" * 64) ) assert channel.remote_head() == before def test_tx_node_history_rewrite_is_rejected(tmp_path: Path) -> None: _remote, work, tx_root = repository_fixture(tmp_path) channel = GitAcknowledgementChannel(work) outcome = channel.publish( acknowledgement(CHANNEL_READY_OBSERVED, "1" * 40, "2" * 64) ) assert tx_root is not None _git( ["push", "--force", "origin", f"{tx_root}:refs/heads/tx-node"], work, ) with pytest.raises(ResultHistoryRewritten): channel.commits_since(outcome.commit) def test_fake_two_way_flow_is_event_safe_and_uses_no_tx( tmp_path: Path, ) -> None: _remote, work, _tx_root = repository_fixture(tmp_path) rx_channel = GitResultChannel(work) tx_channel = GitAcknowledgementChannel(work) ready = channel_ready() ready_outcome = rx_channel.publish(ready) complete = channel_complete(ready) complete_outcome = rx_channel.publish(complete) state = TxWatcherState() executor = FakeExecutor() outcomes = process_history_cycle( rx_channel, state, executor, now_function=lambda: NOW + timedelta(seconds=2), input_function=lambda _prompt: "AUTHORIZE RF TX", save_function=lambda _state: None, output_function=lambda _text: None, expected_source_head=SOURCE_HEAD, acknowledgement_publisher=tx_channel, main_watcher_pid=4321, ) assert outcomes == [ (ready_outcome.commit, "CHANNEL_TEST_READY"), (complete_outcome.commit, "CHANNEL_TEST_COMPLETE"), ] confirmations = channel_test_acknowledgements( tx_channel, expected_source_head=SOURCE_HEAD, ready_source_commit=ready_outcome.commit, ready_source_digest=result_digest(ready), complete_source_commit=complete_outcome.commit, complete_source_digest=result_digest(complete), ) assert confirmations.ready_confirmed assert confirmations.complete_confirmed assert executor.calls == 0 assert state.last_authorized_digest is None def test_rx_watcher_confirms_both_main_acknowledgements( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: _remote, work, _tx_root = repository_fixture(tmp_path) rx_channel = GitResultChannel(work) tx_channel = GitAcknowledgementChannel(work) ready = channel_ready() ready_outcome = rx_channel.publish(ready) complete = channel_complete(ready) complete_outcome = rx_channel.publish(complete) tx_channel.publish( acknowledgement( CHANNEL_READY_OBSERVED, ready_outcome.commit, result_digest(ready), ) ) tx_channel.publish( acknowledgement( CHANNEL_COMPLETE_OBSERVED, complete_outcome.commit, result_digest(complete), observed_at=(NOW + timedelta(seconds=1)).isoformat(), ) ) monkeypatch.setattr( rx_agent_watcher, "CHANNEL_TEST_READY_COMMIT", ready_outcome.commit, ) monkeypatch.setattr( rx_agent_watcher, "CHANNEL_TEST_READY_DIGEST", result_digest(ready), ) monkeypatch.setattr( rx_agent_watcher, "CHANNEL_TEST_COMPLETE_COMMIT", complete_outcome.commit, ) monkeypatch.setattr( rx_agent_watcher, "CHANNEL_TEST_COMPLETE_DIGEST", result_digest(complete), ) state = rx_agent_watcher.WatcherState() output: list[str] = [] rx_agent_watcher.confirm_main_channel_test( work, state, output_function=output.append, ) assert state.channel_test_ready_digest == result_digest(ready) assert state.channel_test_complete_digest == result_digest(complete) assert state.channel_test_ready_ack_commit is not None assert state.channel_test_complete_ack_commit is not None assert "MAIN READY RECEIVED = CONFIRMED" in output assert "MAIN COMPLETE RECEIVED = CONFIRMED" in output def test_ack_push_failure_does_not_advance_rx_cursor(tmp_path: Path) -> None: _remote, work, _tx_root = repository_fixture(tmp_path) rx_channel = GitResultChannel(work) ready_outcome = rx_channel.publish(channel_ready()) class FailingPublisher: def publish(self, acknowledgement: TxHandoffAcknowledgement) -> object: del acknowledgement raise ResultChannelError("simulated acknowledgement push failure") state = TxWatcherState() with pytest.raises(ResultChannelError, match="simulated acknowledgement"): process_history_cycle( rx_channel, state, FakeExecutor(), now_function=lambda: NOW, input_function=lambda _prompt: "DENY", save_function=lambda _state: None, output_function=lambda _text: None, expected_source_head=SOURCE_HEAD, acknowledgement_publisher=FailingPublisher(), ) assert state.last_processed_rx_node_commit is None assert state.channel_test_ready_digest is None assert ready_outcome.commit != state.last_seen_commit