Add MAIN-to-RX acknowledgement channel
This commit is contained in:
594
tools/tx_handoff_channel.py
Normal file
594
tools/tx_handoff_channel.py
Normal file
@@ -0,0 +1,594 @@
|
||||
"""Strict MAIN-to-RX acknowledgement channel stored on ``tx-node``."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass, fields
|
||||
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 = 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_STARTED = "TX_STARTED"
|
||||
TX_COMPLETE = "TX_COMPLETE"
|
||||
TX_ACK_STATES = frozenset(
|
||||
(
|
||||
CHANNEL_READY_OBSERVED,
|
||||
CHANNEL_COMPLETE_OBSERVED,
|
||||
RX_READY_OBSERVED,
|
||||
TX_AUTHORIZATION_CREATED,
|
||||
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
|
||||
|
||||
|
||||
@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 != TX_ACK_SCHEMA_VERSION:
|
||||
raise ValueError(f"schema_version must be {TX_ACK_SCHEMA_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")
|
||||
_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")
|
||||
|
||||
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 (
|
||||
not acknowledgement.rf_tx_authorized
|
||||
or acknowledgement.rf_tx_started
|
||||
or acknowledgement.rf_tx != 0
|
||||
)
|
||||
):
|
||||
raise ValueError("TX authorization state is inconsistent")
|
||||
if acknowledgement.state in {TX_STARTED, TX_COMPLETE} and not (
|
||||
acknowledgement.rf_tx_authorized
|
||||
and acknowledgement.rf_tx_started
|
||||
and acknowledgement.rf_tx == 1
|
||||
):
|
||||
raise ValueError("TX execution state is inconsistent")
|
||||
return acknowledgement
|
||||
|
||||
|
||||
def acknowledgement_from_mapping(
|
||||
raw: Mapping[str, Any],
|
||||
) -> TxHandoffAcknowledgement:
|
||||
expected_fields = {field.name for field in fields(TxHandoffAcknowledgement)}
|
||||
actual_fields = set(raw)
|
||||
if actual_fields != expected_fields:
|
||||
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: raw[name] for name in expected_fields}
|
||||
)
|
||||
return validate_acknowledgement(acknowledgement)
|
||||
|
||||
|
||||
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}",
|
||||
"",
|
||||
"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
|
||||
):
|
||||
return commit
|
||||
return None
|
||||
|
||||
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"
|
||||
)
|
||||
push = self._run(
|
||||
[
|
||||
"push",
|
||||
"--quiet",
|
||||
self.remote,
|
||||
f"HEAD:refs/heads/{self.branch}",
|
||||
],
|
||||
cwd=checkout,
|
||||
check=False,
|
||||
)
|
||||
if push.returncode != 0:
|
||||
raise ResultChannelError(
|
||||
f"acknowledgement push failed: {push.stderr.strip()}"
|
||||
)
|
||||
remote_after = self.remote_head()
|
||||
if remote_after != commit:
|
||||
raise ResultChannelError(
|
||||
"remote tx-node HEAD does not confirm the published commit"
|
||||
)
|
||||
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",
|
||||
]
|
||||
Reference in New Issue
Block a user