fix(clients): resolve 2026-06-18 array-write review findings
- Client.Dotnet-030: add advise-supervisory to IsKnownGatewayCommand (was dead/unreachable, exit 2) - Client.Go-035/036/037: usage+README list advise-supervisory; add session-id guard test; fix write2 README wording - Client.Python-037/038: drop regressed 'scaffold' from pyproject; add advise-supervisory CLI tests - Client.Rust-039/040: document write_array_elements/advise-supervisory in design doc; pin outer MxValue data_type==0 - Client.Java-049/050/051: sync CLIENT_VERSION to 0.1.2; add advise-supervisory test; guard negative uint32 inputs (pending windev gradle verification) Client READMEs also updated for Server-057 add-family normalization wording. .NET/Go/Python/Rust verified green locally; Java pending windev.
This commit is contained in:
@@ -170,8 +170,9 @@ await session.write_array_elements(
|
||||
```
|
||||
|
||||
Bare-name array items (e.g. `Object.ArrayAttr` without an index suffix) added
|
||||
via `add_item` auto-normalize to `[]` — they refer to the whole array, not a
|
||||
single element. Writes through such handles must cover the full array or use
|
||||
via `add_item`, `add_item2`, `add_item_bulk`, or `add_buffered_item`
|
||||
auto-normalize to `[]` — they refer to the whole array, not a single element.
|
||||
Writes through such handles must cover the full array or use
|
||||
`write_array_elements` to supply `total_length` and let the gateway fill
|
||||
defaults for the rest.
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
[project]
|
||||
name = "zb-mom-ww-mxaccess-gateway-client"
|
||||
version = "0.1.2"
|
||||
description = "Async Python client scaffold for MXAccess Gateway."
|
||||
description = "Async Python client for MXAccess Gateway."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
"""Regression tests for Client.Python-037 and Client.Python-038.
|
||||
|
||||
Client.Python-037: ``pyproject.toml`` description must not contain "scaffold".
|
||||
Client.Python-038: ``advise-supervisory`` CLI subcommand must have coverage
|
||||
(registration smoke test + happy-path command-shape test).
|
||||
|
||||
Tests are TDD-first — written before the fix and expected to pass once the
|
||||
source change lands.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from click.testing import CliRunner
|
||||
|
||||
from zb_mom_ww_mxgateway import ClientOptions, GatewayClient
|
||||
from zb_mom_ww_mxgateway.generated import mxaccess_gateway_pb2 as pb
|
||||
from zb_mom_ww_mxgateway_cli.commands import main
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Client.Python-037 — pyproject.toml description must not contain "scaffold".
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_pyproject_description_does_not_contain_scaffold() -> None:
|
||||
"""The ``description`` field in ``pyproject.toml`` must not include the
|
||||
word "scaffold" — a regression of Client.Python-001 that re-entered the
|
||||
file at the package-rename commit.
|
||||
"""
|
||||
|
||||
pyproject = (
|
||||
Path(__file__).resolve().parent.parent / "pyproject.toml"
|
||||
).read_text(encoding="utf-8")
|
||||
|
||||
# Find the description line and assert "scaffold" is absent.
|
||||
for line in pyproject.splitlines():
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("description"):
|
||||
assert "scaffold" not in stripped.lower(), (
|
||||
f"pyproject.toml description must not contain 'scaffold': {stripped!r}"
|
||||
)
|
||||
return
|
||||
|
||||
raise AssertionError("pyproject.toml has no 'description' line")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Client.Python-038 — advise-supervisory must be registered + have a happy path.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_advise_supervisory_is_registered() -> None:
|
||||
"""``advise-supervisory`` must be a registered subcommand of ``main``.
|
||||
|
||||
A ``--help`` invocation must exit 0 and the help text must include the
|
||||
required options (--server-handle and --item-handle).
|
||||
"""
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["advise-supervisory", "--help"])
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "--server-handle" in result.output
|
||||
assert "--item-handle" in result.output
|
||||
|
||||
|
||||
# --------------- fake-stub infrastructure (mirrors test_review_findings_022_to_026) ----
|
||||
|
||||
|
||||
class _FakeUnary:
|
||||
def __init__(self, replies: list[Any]) -> None:
|
||||
self.replies = replies
|
||||
self.requests: list[Any] = []
|
||||
self.metadata: tuple[tuple[str, str], ...] | None = None
|
||||
|
||||
async def __call__(self, request: Any, *, metadata: tuple[tuple[str, str], ...]) -> Any:
|
||||
self.requests.append(request)
|
||||
self.metadata = metadata
|
||||
return self.replies.pop(0)
|
||||
|
||||
|
||||
class _FakeStub:
|
||||
def __init__(self) -> None:
|
||||
self.open_session = _FakeUnary(
|
||||
[
|
||||
pb.OpenSessionReply(
|
||||
session_id="session-1",
|
||||
protocol_status=pb.ProtocolStatus(code=pb.PROTOCOL_STATUS_CODE_OK),
|
||||
),
|
||||
],
|
||||
)
|
||||
self.invoke = _FakeUnary([])
|
||||
self.OpenSession = self.open_session
|
||||
self.Invoke = self.invoke
|
||||
|
||||
def set_invoke_replies(self, replies: list[Any]) -> None:
|
||||
self.invoke.replies = replies
|
||||
|
||||
|
||||
def _install_fake_connect(monkeypatch: Any, stub: _FakeStub) -> None:
|
||||
"""Patch ``GatewayClient.connect`` so the CLI uses the supplied fake stub."""
|
||||
|
||||
real_connect = GatewayClient.connect
|
||||
|
||||
@classmethod # type: ignore[misc]
|
||||
async def _spy_connect(cls: Any, options: ClientOptions, **kwargs: Any) -> GatewayClient:
|
||||
return await real_connect(options, stub=stub)
|
||||
|
||||
monkeypatch.setattr(GatewayClient, "connect", _spy_connect)
|
||||
|
||||
|
||||
def test_cli_advise_supervisory_happy_path(monkeypatch: Any) -> None:
|
||||
"""``advise-supervisory`` must forward server_handle and item_handle in an
|
||||
``MX_COMMAND_KIND_ADVISE_SUPERVISORY`` ``MxCommand``.
|
||||
|
||||
Pattern mirrors ``test_cli_acknowledge_alarm_happy_path`` in
|
||||
``test_review_findings_022_to_026.py``.
|
||||
"""
|
||||
|
||||
stub = _FakeStub()
|
||||
stub.set_invoke_replies(
|
||||
[
|
||||
pb.MxCommandReply(
|
||||
session_id="session-1",
|
||||
kind=pb.MX_COMMAND_KIND_ADVISE_SUPERVISORY,
|
||||
protocol_status=pb.ProtocolStatus(code=pb.PROTOCOL_STATUS_CODE_OK),
|
||||
),
|
||||
],
|
||||
)
|
||||
_install_fake_connect(monkeypatch, stub)
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
main,
|
||||
[
|
||||
"advise-supervisory",
|
||||
"--endpoint",
|
||||
"localhost:5000",
|
||||
"--plaintext",
|
||||
"--session-id",
|
||||
"session-1",
|
||||
"--server-handle",
|
||||
"7",
|
||||
"--item-handle",
|
||||
"42",
|
||||
"--json",
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
payload = json.loads(result.output)
|
||||
assert payload["ok"] is True
|
||||
|
||||
# Verify the MxCommand shape forwarded to the gateway.
|
||||
assert len(stub.invoke.requests) == 1
|
||||
cmd = stub.invoke.requests[0].command
|
||||
assert cmd.kind == pb.MX_COMMAND_KIND_ADVISE_SUPERVISORY
|
||||
assert cmd.advise_supervisory.server_handle == 7
|
||||
assert cmd.advise_supervisory.item_handle == 42
|
||||
Reference in New Issue
Block a user