"""Strict MAIN-to-RX acknowledgement channel stored on ``tx-node``.""" from __future__ import annotations from dataclasses import asdict, dataclass, fields, replace from datetime import datetime, timezone import hashlib import json from pathlib import Path import re import tempfile from typing import Any, Mapping from tools.rf_handoff_channel import ( CHANNEL_TEST_TASK_ID, MAX_RESULT_BYTES, PublishOutcome, ResultBranchMissing, ResultChannelError, ResultHistoryRewritten, GitResultChannel, ) TX_ACK_SCHEMA_VERSION = 2 LEGACY_TX_ACK_SCHEMA_VERSION = 1 TX_RESULT_JSON_PATH = "docs/handoff/results/TX_RESULT.json" TX_RESULT_MARKDOWN_PATH = "docs/handoff/results/TX_RESULT.md" TX_RESULT_PATHS = frozenset((TX_RESULT_JSON_PATH, TX_RESULT_MARKDOWN_PATH)) TX_NODE_BRANCH = "tx-node" CHANNEL_READY_OBSERVED = "CHANNEL_READY_OBSERVED" CHANNEL_COMPLETE_OBSERVED = "CHANNEL_COMPLETE_OBSERVED" RX_READY_OBSERVED = "RX_READY_OBSERVED" TX_AUTHORIZATION_CREATED = "TX_AUTHORIZATION_CREATED" TX_AUTHORIZED = "TX_AUTHORIZED" TX_STARTED = "TX_STARTED" TX_COMPLETE = "TX_COMPLETE" TX_ACK_STATES = frozenset( ( CHANNEL_READY_OBSERVED, CHANNEL_COMPLETE_OBSERVED, RX_READY_OBSERVED, TX_AUTHORIZATION_CREATED, TX_AUTHORIZED, TX_STARTED, TX_COMPLETE, ) ) CHANNEL_TEST_READY_COMMIT = "4cb6c8cad1ab5194755c2740c2a5a83933d099d8" CHANNEL_TEST_READY_DIGEST = ( "23fc54f1f788f068fd4a842daf0a445bd4ec192866b4d89406c74f1797a9952b" ) CHANNEL_TEST_COMPLETE_COMMIT = "83d91ff5309777ab639a24f774e9d0b8825a11e9" CHANNEL_TEST_COMPLETE_DIGEST = ( "7d98ed2e70e98a087b968c0d65fb1db4277b81f251e075d20583de2e5002b2d6" ) _HEAD_RE = re.compile(r"^[0-9a-f]{40}$") _SHA256_RE = re.compile(r"^[0-9a-f]{64}$") _TASK_ID_RE = re.compile(r"^[A-Z0-9][A-Z0-9_-]{2,79}$") @dataclass(frozen=True) class TxHandoffAcknowledgement: """One strict acknowledgement of an exact event from ``rx-node``.""" schema_version: int task_id: str state: str source_rx_commit: str source_rx_digest: str source_head: str observed_at: str main_watcher_pid: int rf_tx_authorized: bool rf_tx_started: bool rf_tx: int tx_task_id: str | None = None tx_started_at: str | None = None tx_finished_at: str | None = None tx_process_exit_code: int | None = None tx_underruns: int | None = None tx_errors: int | None = None @dataclass(frozen=True) class ChannelTestAcknowledgements: ready_confirmed: bool complete_confirmed: bool ready_ack_commit: str | None complete_ack_commit: str | None def _parse_utc(value: str) -> datetime: try: parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) except (TypeError, ValueError) as error: raise ValueError("observed_at must be an ISO-8601 timestamp") from error if parsed.tzinfo is None: raise ValueError("observed_at must contain a timezone") return parsed.astimezone(timezone.utc) def validate_acknowledgement( acknowledgement: TxHandoffAcknowledgement, ) -> TxHandoffAcknowledgement: if type(acknowledgement.schema_version) is not int: raise ValueError("schema_version must contain an integer") if acknowledgement.schema_version not in { LEGACY_TX_ACK_SCHEMA_VERSION, TX_ACK_SCHEMA_VERSION, }: raise ValueError( "schema_version must be a supported TX acknowledgement version" ) string_values = ( acknowledgement.task_id, acknowledgement.state, acknowledgement.source_rx_commit, acknowledgement.source_rx_digest, acknowledgement.source_head, acknowledgement.observed_at, ) if any(type(value) is not str for value in string_values): raise ValueError("acknowledgement string fields must contain strings") if not _TASK_ID_RE.fullmatch(acknowledgement.task_id): raise ValueError("task_id is invalid") if acknowledgement.state not in TX_ACK_STATES: raise ValueError("state is not supported") if not _HEAD_RE.fullmatch(acknowledgement.source_rx_commit): raise ValueError("source_rx_commit must be a lowercase 40-character hash") if not _SHA256_RE.fullmatch(acknowledgement.source_rx_digest): raise ValueError("source_rx_digest must be a lowercase SHA256") if not _HEAD_RE.fullmatch(acknowledgement.source_head): raise ValueError("source_head must be a lowercase 40-character hash") observed_at = _parse_utc(acknowledgement.observed_at) if ( type(acknowledgement.main_watcher_pid) is not int or acknowledgement.main_watcher_pid <= 0 ): raise ValueError("main_watcher_pid must be a positive integer") if type(acknowledgement.rf_tx_authorized) is not bool: raise ValueError("rf_tx_authorized must contain a boolean") if type(acknowledgement.rf_tx_started) is not bool: raise ValueError("rf_tx_started must contain a boolean") if type(acknowledgement.rf_tx) is not int or acknowledgement.rf_tx not in {0, 1}: raise ValueError("rf_tx must be 0 or 1") optional_integers = ( acknowledgement.tx_process_exit_code, acknowledgement.tx_underruns, acknowledgement.tx_errors, ) if any(value is not None and type(value) is not int for value in optional_integers): raise ValueError("TX counters and exit code must contain integers or null") if any( value is not None and value < 0 for value in (acknowledgement.tx_underruns, acknowledgement.tx_errors) ): raise ValueError("TX error counters cannot be negative") optional_strings = ( acknowledgement.tx_task_id, acknowledgement.tx_started_at, acknowledgement.tx_finished_at, ) if any(value is not None and type(value) is not str for value in optional_strings): raise ValueError("optional TX fields must contain strings or null") if ( acknowledgement.tx_task_id is not None and not _TASK_ID_RE.fullmatch(acknowledgement.tx_task_id) ): raise ValueError("tx_task_id is invalid") tx_started_at = ( _parse_utc(acknowledgement.tx_started_at) if acknowledgement.tx_started_at is not None else None ) tx_finished_at = ( _parse_utc(acknowledgement.tx_finished_at) if acknowledgement.tx_finished_at is not None else None ) if ( tx_started_at is not None and tx_finished_at is not None and tx_finished_at < tx_started_at ): raise ValueError("tx_finished_at cannot precede tx_started_at") if acknowledgement.schema_version == LEGACY_TX_ACK_SCHEMA_VERSION and any( value is not None for value in ( acknowledgement.tx_task_id, acknowledgement.tx_started_at, acknowledgement.tx_finished_at, acknowledgement.tx_process_exit_code, acknowledgement.tx_underruns, acknowledgement.tx_errors, ) ): raise ValueError("legacy acknowledgement cannot contain TX event metadata") if ( acknowledgement.state in { CHANNEL_READY_OBSERVED, CHANNEL_COMPLETE_OBSERVED, RX_READY_OBSERVED, } and ( acknowledgement.rf_tx_authorized or acknowledgement.rf_tx_started or acknowledgement.rf_tx != 0 ) ): raise ValueError("observation states cannot claim RF TX authorization") if acknowledgement.state in { CHANNEL_READY_OBSERVED, CHANNEL_COMPLETE_OBSERVED, } and acknowledgement.task_id != CHANNEL_TEST_TASK_ID: raise ValueError("channel acknowledgement has the wrong task_id") if acknowledgement.state == TX_AUTHORIZATION_CREATED and ( acknowledgement.schema_version != LEGACY_TX_ACK_SCHEMA_VERSION or not acknowledgement.rf_tx_authorized or acknowledgement.rf_tx_started or acknowledgement.rf_tx != 0 ): raise ValueError("legacy TX authorization state is inconsistent") if acknowledgement.state == TX_AUTHORIZED and ( acknowledgement.schema_version != TX_ACK_SCHEMA_VERSION or not acknowledgement.rf_tx_authorized or acknowledgement.rf_tx_started or acknowledgement.rf_tx != 0 or acknowledgement.tx_task_id is None or acknowledgement.tx_started_at is not None or acknowledgement.tx_finished_at is not None or acknowledgement.tx_process_exit_code is not None ): raise ValueError("TX authorization state is inconsistent") if acknowledgement.state == TX_STARTED and not ( acknowledgement.rf_tx_authorized and acknowledgement.rf_tx_started and acknowledgement.rf_tx == 1 and acknowledgement.tx_task_id is not None and acknowledgement.tx_started_at is not None and acknowledgement.tx_finished_at is None and acknowledgement.tx_process_exit_code is None ): raise ValueError("TX start state is inconsistent") if acknowledgement.state == TX_STARTED and ( tx_started_at is None or observed_at < tx_started_at ): raise ValueError("TX start observation cannot precede process start") if acknowledgement.state == TX_COMPLETE and not ( acknowledgement.rf_tx_authorized and acknowledgement.rf_tx_started and acknowledgement.rf_tx == 1 and acknowledgement.tx_task_id is not None and acknowledgement.tx_started_at is not None and acknowledgement.tx_finished_at is not None and acknowledgement.tx_process_exit_code is not None and acknowledgement.tx_underruns is not None and acknowledgement.tx_errors is not None ): raise ValueError("TX completion state is inconsistent") if acknowledgement.state == TX_COMPLETE and ( tx_finished_at is None or observed_at < tx_finished_at ): raise ValueError("TX completion observation cannot precede process finish") if acknowledgement.state in { CHANNEL_READY_OBSERVED, CHANNEL_COMPLETE_OBSERVED, RX_READY_OBSERVED, } and any( value is not None for value in ( acknowledgement.tx_task_id, acknowledgement.tx_started_at, acknowledgement.tx_finished_at, acknowledgement.tx_process_exit_code, acknowledgement.tx_underruns, acknowledgement.tx_errors, ) ): raise ValueError("observation state cannot contain TX event metadata") return acknowledgement def build_tx_execution_event( authorization: TxHandoffAcknowledgement, *, state: str, observed_at: str, tx_started_at: str, tx_finished_at: str | None = None, process_exit_code: int | None = None, underruns: int | None = None, errors: int | None = None, ) -> TxHandoffAcknowledgement: """Построить строгое событие TX_STARTED или TX_COMPLETE из разрешения.""" validate_acknowledgement(authorization) if authorization.state != TX_AUTHORIZED: raise ValueError("TX execution event requires a TX_AUTHORIZED source") if state not in {TX_STARTED, TX_COMPLETE}: raise ValueError("TX execution event state is not supported") event = replace( authorization, state=state, observed_at=observed_at, rf_tx_started=True, rf_tx=1, tx_started_at=tx_started_at, tx_finished_at=tx_finished_at, tx_process_exit_code=process_exit_code, tx_underruns=underruns, tx_errors=errors, ) return validate_acknowledgement(event) def acknowledgement_from_mapping( raw: Mapping[str, Any], ) -> TxHandoffAcknowledgement: expected_fields = {field.name for field in fields(TxHandoffAcknowledgement)} optional_event_fields = { "tx_task_id", "tx_started_at", "tx_finished_at", "tx_process_exit_code", "tx_underruns", "tx_errors", } actual_fields = set(raw) schema_version = raw.get("schema_version") legacy_fields = expected_fields - optional_event_fields if schema_version == LEGACY_TX_ACK_SCHEMA_VERSION and actual_fields == legacy_fields: normalized = {**raw, **dict.fromkeys(optional_event_fields)} elif actual_fields == expected_fields: normalized = dict(raw) else: missing = sorted(expected_fields - actual_fields) extra = sorted(actual_fields - expected_fields) raise ValueError( f"acknowledgement fields disagree: missing={missing}, extra={extra}" ) acknowledgement = TxHandoffAcknowledgement( **{name: normalized[name] for name in expected_fields} ) return validate_acknowledgement(acknowledgement) def load_acknowledgement(path: Path) -> TxHandoffAcknowledgement: """Прочитать строгое малое TX-событие из локального JSON.""" raw = json.loads(path.read_text(encoding="utf-8-sig")) if not isinstance(raw, dict): raise ValueError("TX acknowledgement root must be a JSON object") return acknowledgement_from_mapping(raw) def canonical_acknowledgement_json( acknowledgement: TxHandoffAcknowledgement, ) -> str: validate_acknowledgement(acknowledgement) return json.dumps( asdict(acknowledgement), indent=2, sort_keys=True, ensure_ascii=False, ) + "\n" def acknowledgement_digest(acknowledgement: TxHandoffAcknowledgement) -> str: return hashlib.sha256( canonical_acknowledgement_json(acknowledgement).encode("utf-8") ).hexdigest() def render_acknowledgement_markdown( acknowledgement: TxHandoffAcknowledgement, ) -> str: validate_acknowledgement(acknowledgement) return "\n".join( ( "# MAIN acknowledgement", "", f"SCHEMA_VERSION = {acknowledgement.schema_version}", f"TASK_ID = {acknowledgement.task_id}", f"STATE = {acknowledgement.state}", f"SOURCE_RX_COMMIT = {acknowledgement.source_rx_commit}", f"SOURCE_RX_DIGEST = {acknowledgement.source_rx_digest}", f"SOURCE_HEAD = {acknowledgement.source_head}", f"OBSERVED_AT = {acknowledgement.observed_at}", f"MAIN_WATCHER_PID = {acknowledgement.main_watcher_pid}", "RF_TX_AUTHORIZED = " f"{'YES' if acknowledgement.rf_tx_authorized else 'NO'}", f"RF_TX_STARTED = {'YES' if acknowledgement.rf_tx_started else 'NO'}", f"RF_TX = {acknowledgement.rf_tx}", f"TX_TASK_ID = {acknowledgement.tx_task_id}", f"TX_STARTED_AT = {acknowledgement.tx_started_at}", f"TX_FINISHED_AT = {acknowledgement.tx_finished_at}", f"TX_PROCESS_EXIT_CODE = {acknowledgement.tx_process_exit_code}", f"TX_UNDERRUNS = {acknowledgement.tx_underruns}", f"TX_ERRORS = {acknowledgement.tx_errors}", "", "Raw IQ, video, waveforms, source code, local state and large logs " "are not stored in this branch.", "", ) ) class GitAcknowledgementChannel(GitResultChannel): """Read and publish strict acknowledgements on an independent Git branch.""" def __init__( self, repository: Path, *, remote: str = "origin", branch: str = TX_NODE_BRANCH, ) -> None: super().__init__(repository, remote=remote, branch=branch) def read(self) -> tuple[TxHandoffAcknowledgement, str, str]: remote_head = self.fetch() return self.read_commit(remote_head) def read_commit( self, commit: str, ) -> tuple[TxHandoffAcknowledgement, str, str]: if not _HEAD_RE.fullmatch(commit): raise ResultChannelError( "acknowledgement commit must be a full lowercase Git hash" ) commit_check = self._run( ["cat-file", "-e", f"{commit}^{{commit}}"], check=False, ) if commit_check.returncode != 0: raise ResultChannelError( f"acknowledgement commit is unavailable: {commit}" ) tree_text = self._run( ["ls-tree", "-r", "--name-only", commit] ).stdout tree_paths = { line.strip().replace("\\", "/") for line in tree_text.splitlines() if line.strip() } if tree_paths != TX_RESULT_PATHS: raise ResultChannelError( f"tx-node tree contains unexpected paths: {sorted(tree_paths)}" ) for path in TX_RESULT_PATHS: size_text = self._run(["cat-file", "-s", f"{commit}:{path}"]).stdout if int(size_text.strip()) > MAX_RESULT_BYTES: raise ResultChannelError(f"acknowledgement file exceeds limit: {path}") json_result = self._run( ["show", f"{commit}:{TX_RESULT_JSON_PATH}"], check=False, ) if json_result.returncode != 0: raise ResultChannelError( f"TX_RESULT.json is unavailable at commit {commit}: " f"{json_result.stderr.strip()}" ) raw = json.loads(json_result.stdout) if not isinstance(raw, dict): raise ResultChannelError( f"TX_RESULT.json root is not an object at commit {commit}" ) acknowledgement = acknowledgement_from_mapping(raw) return acknowledgement, acknowledgement_digest(acknowledgement), commit def commits_since( self, last_processed_commit: str | None, ) -> tuple[list[str], str]: remote_head = self.fetch() if last_processed_commit is not None: if not _HEAD_RE.fullmatch(last_processed_commit): raise ResultChannelError( "last processed acknowledgement commit is invalid" ) if self._optional_revision(last_processed_commit) is None: raise ResultHistoryRewritten( "last processed acknowledgement commit is unavailable" ) if not self._is_ancestor(last_processed_commit, remote_head): raise ResultHistoryRewritten( "tx-node no longer descends from the last processed commit" ) if last_processed_commit == remote_head: return [], remote_head history = self._run( [ "rev-list", "--reverse", "--topo-order", f"{last_processed_commit}..{remote_head}", ] ).stdout return ( [line.strip() for line in history.splitlines() if line.strip()], remote_head, ) history = self._run( ["rev-list", "--reverse", "--topo-order", remote_head] ).stdout all_commits = [line.strip() for line in history.splitlines() if line.strip()] first_result_index: int | None = None for index, commit in enumerate(all_commits): exists = self._run( ["cat-file", "-e", f"{commit}:{TX_RESULT_JSON_PATH}"], check=False, ) if exists.returncode == 0: first_result_index = index break if first_result_index is None: return [], remote_head return all_commits[first_result_index:], remote_head def _matching_commit( self, acknowledgement: TxHandoffAcknowledgement, ) -> str | None: commits, _remote_head = self.commits_since(None) for commit in reversed(commits): observed, _digest, _head = self.read_commit(commit) if ( observed.task_id == acknowledgement.task_id and observed.state == acknowledgement.state and observed.source_rx_commit == acknowledgement.source_rx_commit and observed.source_rx_digest == acknowledgement.source_rx_digest and observed.source_head == acknowledgement.source_head and observed.rf_tx_authorized == acknowledgement.rf_tx_authorized and observed.rf_tx_started == acknowledgement.rf_tx_started and observed.rf_tx == acknowledgement.rf_tx and observed.tx_task_id == acknowledgement.tx_task_id and observed.tx_started_at == acknowledgement.tx_started_at and observed.tx_finished_at == acknowledgement.tx_finished_at and observed.tx_process_exit_code == acknowledgement.tx_process_exit_code and observed.tx_underruns == acknowledgement.tx_underruns and observed.tx_errors == acknowledgement.tx_errors ): return commit return None @staticmethod def _same_execution_binding( left: TxHandoffAcknowledgement, right: TxHandoffAcknowledgement, ) -> bool: return ( left.task_id == right.task_id and left.source_rx_commit == right.source_rx_commit and left.source_rx_digest == right.source_rx_digest and left.source_head == right.source_head and left.tx_task_id == right.tx_task_id ) def publish_execution_event( self, event: TxHandoffAcknowledgement, ) -> PublishOutcome: """Опубликовать TX-событие только после связанного TX_AUTHORIZED.""" validate_acknowledgement(event) if event.state not in {TX_STARTED, TX_COMPLETE}: raise ResultChannelError("execution event must be TX_STARTED or TX_COMPLETE") commits, _remote_head = self.commits_since(None) authorization_found = False start_found = event.state == TX_STARTED for commit in commits: observed, _digest, _head = self.read_commit(commit) if not self._same_execution_binding(observed, event): continue if observed.state == TX_AUTHORIZED: authorization_found = True if ( observed.state == TX_STARTED and observed.tx_started_at == event.tx_started_at ): start_found = True if not authorization_found: raise ResultChannelError("matching TX_AUTHORIZED event is absent") if not start_found: raise ResultChannelError("matching TX_STARTED event is absent") return self.publish(event) def publish( self, acknowledgement: TxHandoffAcknowledgement, ) -> PublishOutcome: validate_acknowledgement(acknowledgement) remote_before = self.fetch() matching_commit = self._matching_commit(acknowledgement) if matching_commit is not None: return PublishOutcome( "UNCHANGED", matching_commit, remote_before, json_path=TX_RESULT_JSON_PATH, markdown_path=TX_RESULT_MARKDOWN_PATH, ) tracking_reference = f"refs/remotes/{self.remote}/{self.branch}" with tempfile.TemporaryDirectory(prefix="tx-ack-worktree-") as directory: checkout = Path(directory) / "tx-node" worktree_added = False try: self._run( ["worktree", "add", "--detach", str(checkout), tracking_reference] ) worktree_added = True existing_text = self._run( ["ls-tree", "-r", "--name-only", "HEAD"], cwd=checkout, ).stdout existing_paths = { line.strip().replace("\\", "/") for line in existing_text.splitlines() if line.strip() } if not existing_paths.issubset(TX_RESULT_PATHS): raise ResultChannelError( "tx-node contains paths outside the acknowledgement allowlist" ) json_path = checkout / TX_RESULT_JSON_PATH markdown_path = checkout / TX_RESULT_MARKDOWN_PATH json_path.parent.mkdir(parents=True, exist_ok=True) json_text = canonical_acknowledgement_json(acknowledgement) markdown_text = render_acknowledgement_markdown(acknowledgement) if len(json_text.encode("utf-8")) > MAX_RESULT_BYTES: raise ResultChannelError("TX_RESULT.json exceeds the size limit") if len(markdown_text.encode("utf-8")) > MAX_RESULT_BYTES: raise ResultChannelError("TX_RESULT.md exceeds the size limit") json_path.write_text(json_text, encoding="utf-8", newline="\n") markdown_path.write_text( markdown_text, encoding="utf-8", newline="\n", ) self._run(["add", "--", *sorted(TX_RESULT_PATHS)], cwd=checkout) staged_text = self._run( ["diff", "--cached", "--name-only", "--"], cwd=checkout, ).stdout staged = { line.strip().replace("\\", "/") for line in staged_text.splitlines() if line.strip() } if staged != TX_RESULT_PATHS: raise ResultChannelError( f"refusing unexpected staged paths: {sorted(staged)}" ) self._run( [ "-c", "user.name=MAIN TX Watcher", "-c", "user.email=tx-node@local.invalid", "commit", "--quiet", "-m", f"Acknowledge {acknowledgement.task_id} " f"{acknowledgement.state}", ], cwd=checkout, ) commit = self._run( ["rev-parse", "HEAD"], cwd=checkout, ).stdout.strip() committed_text = self._run( ["ls-tree", "-r", "--name-only", commit], cwd=checkout, ).stdout committed_paths = { line.strip().replace("\\", "/") for line in committed_text.splitlines() if line.strip() } if committed_paths != TX_RESULT_PATHS: raise ResultChannelError( "committed tx-node tree violates the path allowlist" ) self._push_exact_commit( checkout, commit, remote_before, ) return PublishOutcome( "PUBLISHED", commit, commit, json_path=TX_RESULT_JSON_PATH, markdown_path=TX_RESULT_MARKDOWN_PATH, ) finally: if worktree_added: removal = self._run( ["worktree", "remove", "--force", str(checkout)], check=False, ) if removal.returncode != 0: raise ResultChannelError( "temporary acknowledgement worktree cleanup failed: " f"{removal.stderr.strip()}" ) self._run(["worktree", "prune"], check=False) def channel_test_acknowledgements( channel: GitAcknowledgementChannel, *, expected_source_head: str, ready_source_commit: str = CHANNEL_TEST_READY_COMMIT, ready_source_digest: str = CHANNEL_TEST_READY_DIGEST, complete_source_commit: str = CHANNEL_TEST_COMPLETE_COMMIT, complete_source_digest: str = CHANNEL_TEST_COMPLETE_DIGEST, ) -> ChannelTestAcknowledgements: """Find both exact channel-test acknowledgements in chronological history.""" if not _HEAD_RE.fullmatch(expected_source_head): raise ValueError("expected_source_head must be a full lowercase Git hash") if not _HEAD_RE.fullmatch(ready_source_commit): raise ValueError("ready_source_commit must be a full lowercase Git hash") if not _SHA256_RE.fullmatch(ready_source_digest): raise ValueError("ready_source_digest must be a lowercase SHA256") if not _HEAD_RE.fullmatch(complete_source_commit): raise ValueError("complete_source_commit must be a full lowercase Git hash") if not _SHA256_RE.fullmatch(complete_source_digest): raise ValueError("complete_source_digest must be a lowercase SHA256") commits, _remote_head = channel.commits_since(None) ready_commit: str | None = None complete_commit: str | None = None for commit in commits: acknowledgement, _digest, _head = channel.read_commit(commit) common = ( acknowledgement.task_id == CHANNEL_TEST_TASK_ID and acknowledgement.source_head == expected_source_head and not acknowledgement.rf_tx_authorized and not acknowledgement.rf_tx_started and acknowledgement.rf_tx == 0 ) if ( common and acknowledgement.state == CHANNEL_READY_OBSERVED and acknowledgement.source_rx_commit == ready_source_commit and acknowledgement.source_rx_digest == ready_source_digest ): ready_commit = commit elif ( common and ready_commit is not None and acknowledgement.state == CHANNEL_COMPLETE_OBSERVED and acknowledgement.source_rx_commit == complete_source_commit and acknowledgement.source_rx_digest == complete_source_digest ): complete_commit = commit return ChannelTestAcknowledgements( ready_confirmed=ready_commit is not None, complete_confirmed=complete_commit is not None, ready_ack_commit=ready_commit, complete_ack_commit=complete_commit, ) __all__ = [ "CHANNEL_COMPLETE_OBSERVED", "CHANNEL_READY_OBSERVED", "CHANNEL_TEST_COMPLETE_COMMIT", "CHANNEL_TEST_COMPLETE_DIGEST", "CHANNEL_TEST_READY_COMMIT", "CHANNEL_TEST_READY_DIGEST", "ChannelTestAcknowledgements", "GitAcknowledgementChannel", "ResultBranchMissing", "ResultChannelError", "ResultHistoryRewritten", "TX_ACK_SCHEMA_VERSION", "TX_NODE_BRANCH", "TX_RESULT_JSON_PATH", "TX_RESULT_MARKDOWN_PATH", "TX_RESULT_PATHS", "TxHandoffAcknowledgement", "acknowledgement_digest", "acknowledgement_from_mapping", "canonical_acknowledgement_json", "channel_test_acknowledgements", "render_acknowledgement_markdown", "validate_acknowledgement", ]