mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-11 19:47:12 +03:00
fix(tempest): honor --dry-run on udp listen without binding a socket
udp listen ignored the universal --dry-run flag and bound UDP 50222, hanging when no hub is on the LAN. Add a dry-run plan branch that describes the listen parameters (bind address, port, timeout, show-all) and exits 0 without creating any socket, so doc claims of universal --dry-run support stay universal and true. Four regression tests pin the plan shape, the defaults/--show-all propagation, and prove no socket is constructed (and no token demanded). Recipe 5 documents the udp listen plan object alongside the other plans. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
parent
2a9a81e29b
commit
ce5f34a33d
@@ -121,16 +121,24 @@ on `type` before indexing.
|
||||
tempest forecast --station-id 12799 --days 3 --dry-run --json
|
||||
# -> {"dry_run": true, "command": "forecast", "station_id": 12799, "days": 3}
|
||||
|
||||
# Every documented command has a dry-run plan: current, obs, forecast, stations
|
||||
# Every documented command has a dry-run plan — current, obs, forecast,
|
||||
# stations, and udp listen (plans the bind, creates no socket, safe off-LAN)
|
||||
tempest obs --device-id 60526 --days 2 --dry-run --json
|
||||
tempest udp listen --port 50222 --timeout 30 --dry-run --json
|
||||
# -> {"dry_run": true, "command": "udp", "subcommand": "listen",
|
||||
# "bind_address": "0.0.0.0", "port": 50222, "timeout_seconds": 30,
|
||||
# "show_all": false}
|
||||
|
||||
# Quiet/verbose piping: logs on stderr, data on stdout
|
||||
tempest current --json --quiet | jq .observation.air_temperature
|
||||
```
|
||||
|
||||
Behavior contract: `--dry-run` works without `TEMPEST_TOKEN` set (no credential
|
||||
needed to see a plan); `--help` and `--dry-run` are always offline. Without
|
||||
`--dry-run`, a missing token exits 1 with
|
||||
needed to see a plan); `--help` and `--dry-run` are always offline. For
|
||||
`udp listen`, dry-run describes the listen parameters (bind address, port,
|
||||
timeout, show-all) and exits 0 without creating or binding any socket — the
|
||||
real listener waits for hub traffic on UDP 50222 and needs the hub's LAN.
|
||||
Without `--dry-run`, a missing token exits 1 with
|
||||
`Error: TEMPEST_TOKEN not set...` before any request is attempted.
|
||||
|
||||
## Recipe 6: JSON error paths you'll actually see
|
||||
|
||||
@@ -732,6 +732,17 @@ def udp_listen(args: argparse.Namespace) -> None:
|
||||
timeout = args.timeout
|
||||
show_all = args.show_all
|
||||
|
||||
if GLOBAL_FLAGS.get("dry_run", False):
|
||||
# Dry-run plans the listen instead of opening it: exits 0 without
|
||||
# creating or binding any socket, so it is safe anywhere (no hub
|
||||
# required, no LAN needed, no hanging on a broadcast port).
|
||||
emit("[dry-run] Would listen for Tempest UDP broadcasts on "
|
||||
f"{UDP_BROADCAST_ADDR}:{port} — binds no socket now.",
|
||||
{"dry_run": True, "command": "udp", "subcommand": "listen",
|
||||
"bind_address": UDP_BROADCAST_ADDR, "port": port,
|
||||
"timeout_seconds": timeout, "show_all": show_all})
|
||||
return
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.bind((UDP_BROADCAST_ADDR, port))
|
||||
|
||||
@@ -316,6 +316,56 @@ class DryRunTests(CliTestCase):
|
||||
self.assertEqual(code, 0)
|
||||
self.assertEqual(json.loads(out)["command"], "stations")
|
||||
|
||||
def test_udp_listen_dry_run_plan_shape(self):
|
||||
# VAL-TEMP-011: udp listen honors --dry-run — a plan JSON, exit 0,
|
||||
# and (pinned by the socket patch below) NO socket is ever created
|
||||
# or bound, so the dry run cannot hang waiting for hub traffic.
|
||||
code, out = run_main(["udp", "listen", "--port", "50222",
|
||||
"--timeout", "30", "--dry-run", "--json"])
|
||||
self.assertEqual(code, 0)
|
||||
plan = json.loads(out)
|
||||
self.assertEqual(plan["dry_run"], True)
|
||||
self.assertEqual(plan["command"], "udp")
|
||||
self.assertEqual(plan["subcommand"], "listen")
|
||||
self.assertEqual(plan["bind_address"], ts.UDP_BROADCAST_ADDR)
|
||||
self.assertEqual(plan["port"], 50222)
|
||||
self.assertEqual(plan["timeout_seconds"], 30)
|
||||
self.assertEqual(plan["show_all"], False)
|
||||
|
||||
def test_udp_listen_dry_run_defaults_and_show_all(self):
|
||||
# Defaults land in the plan; --show-all propagates.
|
||||
code, out = run_main(["udp", "listen", "--show-all", "--dry-run", "--json"])
|
||||
self.assertEqual(code, 0)
|
||||
plan = json.loads(out)
|
||||
self.assertEqual(plan["port"], ts.DEFAULT_UDP_PORT)
|
||||
self.assertEqual(plan["timeout_seconds"], 0)
|
||||
self.assertEqual(plan["show_all"], True)
|
||||
|
||||
def test_udp_listen_dry_run_creates_no_socket(self):
|
||||
# Prove the "binds no socket" half of the contract: if udp_listen
|
||||
# reached its listen path, socket.socket() would be constructed and
|
||||
# this fake's bind() would blow up the test.
|
||||
bound = []
|
||||
|
||||
class NoBindSock:
|
||||
def bind(self, *a, **k):
|
||||
bound.append(a)
|
||||
raise AssertionError("dry-run udp listen must not bind a socket")
|
||||
|
||||
with patch.object(ts.socket, "socket", side_effect=AssertionError(
|
||||
"dry-run udp listen must not create a socket")):
|
||||
code, out = run_main(["udp", "listen", "--dry-run", "--json"])
|
||||
self.assertEqual(code, 0)
|
||||
self.assertEqual(bound, [])
|
||||
self.assertEqual(json.loads(out)["dry_run"], True)
|
||||
|
||||
def test_udp_listen_dry_run_without_token_is_fine(self):
|
||||
# UDP needs no token, and the dry run must not demand one either.
|
||||
with patch_token(""):
|
||||
code, out = run_main(["udp", "listen", "--dry-run", "--json"])
|
||||
self.assertEqual(code, 0)
|
||||
self.assertEqual(json.loads(out)["command"], "udp")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Class 4: mocked REST client logic (no real network anywhere)
|
||||
|
||||
Reference in New Issue
Block a user