Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ All notable changes to the Anolis Device Provider Protocol (ADPP) are documented

## [Unreleased]

### Added

- **§7.3 freshness-hint gate** (core ADPP, non-waivable). `min_timestamp` is
clarified as a hint that constrains *quality*, not *success*: a provider MUST
NOT turn an otherwise-readable signal into an error (notably
`CODE_DEADLINE_EXCEEDED`) solely because the freshness hint is unmet — it
returns `CODE_OK` with best-available values (`test_min_timestamp_hint_is_not_fatal`,
validated hermetically by `test_selftest_freshness_hint_validator`). No
wire/proto change. Closes a silent-divergence surface ahead of the
provider-SDK extraction (anolis-protocol#47); a provider that errored on an
unmet hint becomes non-conformant when it bumps to the harness release
carrying this test.

## [v1.5.0] — 2026-06-23

No wire/proto changes from v1.4.0 — this release is conformance-harness and
Expand Down
19 changes: 19 additions & 0 deletions conformance/anolis_conformance/checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,25 @@ def assert_signalvalues_l2(read_response) -> None:
raise ConformanceFailure(f"signal {sid!r}: quality must not be QUALITY_UNSPECIFIED")


def assert_freshness_hint_not_fatal(read_response, codes: SimpleNamespace) -> None:
"""semantics.md §7.3: ``ReadSignalsRequest.min_timestamp`` is a best-effort
freshness **hint**, not a hard deadline. A provider that cannot satisfy it
MUST return the best available values (``CODE_OK``) and indicate staleness via
``quality``/``Status.details`` — it MUST NOT, by itself, turn an otherwise-
readable signal into an error (notably ``CODE_DEADLINE_EXCEEDED``). A genuine
read failure (``CODE_UNAVAILABLE``) is unrelated and exempt; the caller first
establishes that a hint-free read of the same device succeeds."""
code = read_response.status.code
if code in (codes.OK, codes.UNAVAILABLE):
return
name = next((n for n, v in vars(codes).items() if v == code), str(code))
raise ConformanceFailure(
f"a read with min_timestamp set returned status CODE_{name} — §7.3 freshness is a "
"best-effort hint that constrains *quality*, not *success*: return best-available "
"values (CODE_OK) with QUALITY_STALE, not an error such as CODE_DEADLINE_EXCEEDED"
)


_SNAKE_CASE = re.compile(r"^[a-z][a-z0-9_]*$")


Expand Down
12 changes: 11 additions & 1 deletion conformance/anolis_conformance/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,11 +225,21 @@ def describe_device(self, device_id: str) -> Any:
req.describe_device.device_id = device_id
return self.send_request(req)

def read_signals(self, device_id: str, signal_ids: Sequence[str] | None = None) -> Any:
def read_signals(
self,
device_id: str,
signal_ids: Sequence[str] | None = None,
*,
min_timestamp: tuple[int, int] | None = None,
) -> Any:
req = self.protocol.Request(request_id=self._request_id())
req.read_signals.device_id = device_id
if signal_ids:
req.read_signals.signal_ids.extend(signal_ids)
if min_timestamp is not None:
seconds, nanos = min_timestamp
req.read_signals.min_timestamp.seconds = seconds
req.read_signals.min_timestamp.nanos = nanos
return self.send_request(req)

def call(
Expand Down
33 changes: 32 additions & 1 deletion conformance/anolis_conformance/test_adpp_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@

from __future__ import annotations

import time

import pytest

from . import spec
from .checks import assert_status_present
from .checks import assert_freshness_hint_not_fatal, assert_status_present
from .client import AdppClient


Expand Down Expand Up @@ -132,6 +134,35 @@ def test_default_read_returns_declared_subset(ready_client: AdppClient, codes, s
pytest.skip("no readable device declares signals")


def test_min_timestamp_hint_is_not_fatal(ready_client: AdppClient, codes, status_text) -> None:
# semantics.md §7.3: `ReadSignalsRequest.min_timestamp` is a best-effort
# freshness HINT. A provider that cannot satisfy it MUST return best-available
# values (CODE_OK) — indicating staleness via quality/Status.details — and MUST
# NOT turn an otherwise-readable signal into an error (notably
# CODE_DEADLINE_EXCEEDED). We use a min_timestamp an hour in the future, which
# no real reading can satisfy, so the hint is always unmet.
future = (int(time.time()) + 3600, 0)
checked = 0
for d in ready_client.list_devices().list_devices.devices:
caps = ready_client.describe_device(d.device_id).describe_device.capabilities
if not caps.signals:
continue
# Establish that the device is readable at all without the hint; a mock
# backend may report UNAVAILABLE, in which case the contract is unobservable.
base = ready_client.read_signals(d.device_id)
if base.status.code == codes.UNAVAILABLE:
continue
assert base.status.code == codes.OK, (
f"baseline (hint-free) read must be OK or UNAVAILABLE; got {status_text(base)}"
)
resp = ready_client.read_signals(d.device_id, min_timestamp=future)
# The unmet freshness hint must not, by itself, fail the read.
assert_freshness_hint_not_fatal(resp, codes)
checked += 1
if checked == 0:
pytest.skip("no readable device declares signals")


def test_read_unknown_signal_consistent(ready_client: AdppClient, codes, status_text) -> None:
# semantics.md 7.4: a provider MUST choose ONE consistent behavior for an
# unknown signal id — either fail CODE_NOT_FOUND, OR return partial results
Expand Down
18 changes: 18 additions & 0 deletions conformance/anolis_conformance/test_selftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from .checks import (
ConformanceFailure,
assert_controlled_malformed,
assert_freshness_hint_not_fatal,
assert_function_ids_per_type_from_one,
assert_signal_ids_snake_case,
assert_signalvalues_l2,
Expand Down Expand Up @@ -282,6 +283,23 @@ def test_selftest_signalvalues_l2_rejects_bad_timestamp(protocol, nanos, seconds
assert_signalvalues_l2(resp)


# --- freshness-hint validator (the real §7.3 check the core suite runs) ---


def test_selftest_freshness_hint_validator(protocol, codes) -> None:
# §7.3: an unmet min_timestamp hint must not be fatal. OK / UNAVAILABLE pass;
# any error code (notably DEADLINE_EXCEEDED) is rejected.
for accepted in ("CODE_OK", "CODE_UNAVAILABLE"):
resp = protocol.Response()
resp.status.code = protocol.Status.Code.Value(accepted)
assert_freshness_hint_not_fatal(resp, codes) # must NOT raise
for rejected in ("CODE_DEADLINE_EXCEEDED", "CODE_INTERNAL", "CODE_FAILED_PRECONDITION"):
resp = protocol.Response()
resp.status.code = protocol.Status.Code.Value(rejected)
with pytest.raises(ConformanceFailure):
assert_freshness_hint_not_fatal(resp, codes)


# --- capability-convention validators (executable profile §4) ---


Expand Down
8 changes: 8 additions & 0 deletions docs/semantics.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,14 @@ If `ReadSignalsRequest.min_timestamp` is provided:
- Providers SHOULD attempt to satisfy the freshness requirement.
- If not feasible, providers SHOULD return the best available values and
indicate staleness via `quality` and/or `Status.details`.
- `min_timestamp` is a **hint that constrains quality, not success**: a provider
MUST NOT treat an unmet freshness hint as a hard deadline. An otherwise-
readable signal MUST NOT, on account of the hint alone, be turned into an error
response (notably `CODE_DEADLINE_EXCEEDED`); the read returns `CODE_OK` with the
best available values. (A genuine read failure may still return
`CODE_UNAVAILABLE` etc.) *(Clarification: previously unstated; a provider that
returned an error solely because the hint was unmet is non-conformant under
this rule.)*

### 7.4 Missing signals

Expand Down
Loading