Release v0.14.22 - #47
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rallel Sprint executed via TeamCreate with 6 teammates coordinated by team-lead. Node.js parity fixes for protocol + graceful lifecycle. ## Tasks completed 1. **Broker hook rename with aliases** (hook-renamer) - middleware/base.py: added short aliases starting/started/stopping - broker.py: _call_middleware_hooks invokes both long + short names - Backward compatible — existing broker_* middleware works unchanged - Skipped `stopped` alias due to collision with existing no-arg cleanup hook 2. **PacketHeartbeat proto schema extension** (proto-extender) - serializers/proto/packets.proto: added fields 4-7 (seq, instanceID, memory, cpuSeq) - Regenerated packets_pb2.py - protobuf.py: _HEARTBEAT_MAX_FIELDS 4 → 8 - ADR-heartbeat-schema.md documenting the decision - Wire-compatible with Node.js (ignores unknown fields) 3. **TrackingConfig in Settings** (tracking-configurator) - settings.py: TrackingConfig dataclass (enabled=False, shutdown_timeout=5.0s) - Validation in Settings._validate() - Exported via moleculerpy/__init__.py 4. **Connection drain on broker stop** (connection-drainer) - transit.py: send_disconnect_info() broadcasts INFO(services=[]) before disconnect - broker.stop(): calls send_disconnect_info() BEFORE transit.disconnect() - Matches Node.js service-broker.js:531-539 pattern - e2e test with 2 memory brokers verifies remote node drops math.add before DISCONNECT 5. **ContextTracker auto-registration** (tracker-integrator) - broker.py __init__: auto-append ContextTrackerMiddleware if settings.tracking.enabled - Converts float seconds → int ms for middleware - e2e test: slow action in-flight, stop() waits for completion 6. **Protocol lifecycle test suite** (test-author) - tests/unit/protocol_lifecycle_test.py (12 tests, ~270 LOC) - System-level integration tests for all 5 changes above ## Evidence - ruff format + check: clean - mypy --strict: 0 errors - pytest: **2396 passed** (2378 baseline + 18 new) - demo_matrix: 28/28 OK - demo_comprehensive: 90/90 OK Refs: Protocol gap audit report Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…INFO, fix caveats
Executed via TeamCreate with 5 parallel teammates. Fixes 3 caveats from previous sprint + adds proper Node.js parity for service registration.
## Protocol parity (the real fix)
**Problem**: Previous sprint extended PacketHeartbeat proto with seq/instanceID/memory/cpuSeq (fields 4-7) to enable heartbeat-driven restart detection. This broke Python↔Node.js cross-language interop.
**Investigation**: Node.js Moleculer doesn't carry seq/instanceID in heartbeat — it relies on IMMEDIATE INFO broadcast on service changes (registry.js: localNodeInfoInvalidated="seq" + servicesChanged → localServiceChanged → sendLocalNodeInfo). Heartbeat carries only {cpu}. The seq/instanceID checks in heartbeatReceived are defensive dead code.
**Fix**:
1. Revert proto schema (fields 4-7 marked `reserved` so never reused)
2. Revert heartbeat payload to {cpu} only
3. Add seq++ + broadcast INFO in broker.register() when connected
4. Keep seq/instanceID checks in _handle_heartbeat as graceful degradation
5. Cross-language interop now works fully via INFO round-trip
## Caveats fixed
1. **stopped alias collision**: signature introspection in _call_middleware_hooks — backward compatible with legacy Middleware.stopped() no-arg cleanup
2. **Heartbeat cross-language**: full fix via revert + seq++ + INFO broadcast
3. **ContextTracker units**: refactored middleware from int ms → float seconds (Python convention)
## Tasks
| # | Agent | Changes |
|---|-------|---------|
| 1 | proto-reverter | Revert PacketHeartbeat schema, fields reserved, ADR updated |
| 2 | heartbeat-reverter | beat() sends {cpu} only |
| 3 | seq-incrementer | register(): seq++ + send_node_info() + e2e test |
| 4 | stopped-alias-fixer | signature introspection for stopped alias |
| 5 | tracker-unit-fixer | ContextTracker → float seconds |
## Evidence
- ruff format + check: clean
- mypy --strict: 0 errors
- pytest: 2403 passed (+7 new)
- demo_matrix: 28/28 OK
- demo_comprehensive: 90/90 OK
Node.js ↔ Python ProtoBuf interop now works for heartbeat + all protocol features.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Critical fixes: - service_starting → service_created (was never dispatched by broker, service tracking was dead code) - _is_tracking_enabled supports TrackingConfig dataclass (was only checking dict) High fixes: - _wait_for_contexts uses monotonic wall-clock deadline instead of elapsed drift - TrackingConfig.__post_init__ validates shutdown_timeout > 0 (prevents 0 → immediate timeout bug) - _HEARTBEAT_MAX_FIELDS = 3 (was 4, off-by-one for 3-field schema) - Alias dispatch guards against double-invoke when middleware overrides both broker_stopped and stopped - ContextTracker auto-register checks for existing instance (prevents double-registration) - broker.register() uses self.node_catalog directly (no transit detour) Medium fixes: - $shutdownTimeout camelCase alias for Node.js parity - Test fixture mock_node_catalog.local_node initialized with seq=0 Evidence: 2403 tests pass, 28/28 demo matrix, 90/90 comprehensive, mypy 0. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
After Sprint Protocol Lifecycle + Sprint Protocol Fixes retro: 1. docs/SPRINT-CHECKLIST.md — Definition of Done checklist for all future sprints: - Protocol & Reference Compliance - Architecture & Design (SRP, no private attr access) - Code Quality & Typing (no getattr abuse, no Any abuse) - Testing (unit + integration + real services) - Documentation (CHANGELOG, ADR, Roadmap) - Audit (3+ agents mandatory for Standard+) - Release Readiness - Technical Debt Tracking - Sprint Retro questions - Red Flags section 2. KNOWN-ISSUES.md — deferred fixes tracker with P0-P3 priorities: - 4 P1 items (seq++ guard, inspect caching, real cross-lang test, proto CI guard) - 5 P2 items (getattr private, God Object, ADR location, stopped hack, Protocol typing) - 6 P3 items (codecov, retro enforcement, fixture pollution, missing regression tests) Going forward every sprint must complete the checklist before merge. Deferred items MUST land in KNOWN-ISSUES.md with priority and effort estimate. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Closes P1 + P2 items from KNOWN-ISSUES.md via parallel TeamCreate sprint. ## Tasks completed (6 teammates, 1 wave) 1. **seq++ re-registration guard** (seq-guard) broker.register() now checks if service already registered before bumping local_node.seq / broadcasting INFO. Prevents duplicate wire storms on hot reload / test teardown+reregister. 2. **inspect.signature caching** (sig-cacher) _alias_args now caches param count per (id(mw), method_name) in self._hook_signature_cache. Reduces repeated introspection on hook dispatch. 3. **ProtoBuf regeneration CI check** (proto-ci) Makefile targets `proto-gen` + `proto-check`, CI workflow fails if packets_pb2.py out of sync with packets.proto. 4. **Transit.is_connected public property** (transit-property) Replaces `getattr(transit, "_was_connected", False)` private access in broker.register() with typed public API. 5. **TrackingConfigProtocol** (tracking-protocol) context_tracker._is_tracking_enabled uses @runtime_checkable Protocol instead of getattr duck-typing. TrackingConfig(enabled=False) now properly detected. 6. **Audit regression tests** (regression-author) New tests/unit/audit_regression_test.py with 7 consolidated regression tests covering: camelCase/snakeCase shutdown timeout keys, double-registration guard, heartbeat proto extra field drop, wall-time deadline, TrackingConfig validation, alias no-double-invoke. ## Cross-language demo stand (bonus) New examples/demo_crosslang.py — REAL Python ↔ Node.js Moleculer cluster: - Pre-flight checks Docker + NATS + Node.js toolchain - Spawns Node.js Moleculer broker subprocess - Spawns Python MoleculerPy broker on same NATS - Tests: discovery, Python→Node RPC, event emit, graceful stop - **Result: 4/4 PASS** — proves cross-language interop works This closes KNOWN-ISSUES P1 item #3 ("No real Python ↔ Node.js cluster test"). The theoretical claim is now backed by a working demo. ## Evidence - ruff format + check: clean - mypy --strict: 0 errors - pytest: **2415 passed** (+12 from sprint) - demo_matrix: 28/28 OK - demo_comprehensive: 90/90 OK - **demo_crosslang: 4/4 PASS** (real Node.js ↔ Python via NATS) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…6/166 tests) Sprint Component Demos via TeamCreate (7 teammates + team-lead). ## New demo stands | Demo | Tests | Verifies on REAL services | |------|-------|---------------------------| | demo_cacher | 7/7 | Memory + LRU + Redis (Valkey 6381) | | demo_channels | 6/6 | Pub/Sub on Redis + NATS | | demo_repl | 10/10 | REPL commands programmatically | | demo_web | 11/11 | HTTP gateway via httpx | | demo_observability | 9/9 | Logging + Metrics (Prometheus) + Tracing | | demo_crosslang | 5/5 | REAL Python ↔ Node.js with file-based feedback | | run_all_demos.py | orchestrator | 8/8 demos with summary table, 131s total | | docs/DEMOS.md | docs | Per-demo documentation | ## Bugs DISCOVERED via real demos (added to KNOWN-ISSUES) P1: - #17 moleculerpy-web route hooks + memory transport hangs broker - #18 EVENT payload Python ships params, Node.js reads data (cross-lang gap) P2: - #16 ChannelsMiddleware drops DeadLetteringOptions instance - #19 MoleculerClientError(code=401) → HTTP 400 instead of 401 P3: - #20 HTTP gateway streaming doesn't drain on broker.stop() ## Cross-language demo (T3/T4/T5 REAL verification) Created tests/integration/node_services/crosslang_test.service.js — Node.js service that writes to /tmp marker files when receiving Python calls/events. Python reads files to verify ACTUAL bidirectional delivery. ## Final orchestrator result 8/8 demos | 166/166 tests | 131s Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… + EVENT schema Tactical P1 fixes from PRD-020 (bug closure sprint v0.14.22). Discovered by real demo stands (demo_web hang, demo_crosslang T4 empty payload), invisible to symmetric Python<->Python unit tests with mocks. #17 — Web route hooks + memory transport hang: node.py build_service_definition previously shipped service.settings as-is. ApiGatewayService stores callable route hooks (onBeforeCall, authorization, ...) in settings — when INFO packet hit json/msgpack/cbor serializers the wire layer crashed or hung mid-serialize. New _serializable_settings helper strips every non-JSON-encodable top-level value using a json.dumps probe (whitelist, not blacklist — also catches datetime, Path, open file, dataclass instances, etc.) and logs a WARNING with the dropped keys so operators see data is being omitted instead of silently losing it. #18 — EVENT payload `params` vs Node.js `data`: Python transit.send_event forwarded context.marshall() which put the event payload under "params" — Node.js handlers read ctx.data and saw undefined. Real root cause: Python conflated REQUEST and EVENT wire schemas through a single Context.marshall(). Node.js has separate packet schemas (transit.js #sendEvent vs #request). Fix keeps the internal Context.params name (REQUEST is unchanged) but builds an explicit EVENT wire payload in send_event that matches Node.js 1:1: {id, event, data, groups, broadcast, meta, level, tracing, parentID, requestID, caller, needAck} A new broadcast=False|True kwarg on send_event is passed from broker's _emit_core (False) and _broadcast_core (True) so receivers can honour emit/broadcast semantics per Moleculer v4 protocol. Receive side: new Lifecycle.rebuild_event_context method owns the EVENT wire schema and translates data -> internal params, with fallback to legacy "params" so a freshly upgraded node keeps accepting traffic from older Python peers mid-rolling-upgrade. REQUEST's rebuild_context is left untouched. Evidence: * tests/unit/audit_regression_test.py — 4 new regression tests locking both fixes (#17 sanitizer keeps JSON, drops callables + returns {} for non-dict; #18 send_event wire schema + receive-side data/params aliasing) * tests/unit/transit_test.py / broker_test.py / event_ack_test.py — updated to match new kwarg (broadcast=...) and rebuild_event_context dispatch so existing coverage tracks the new contract * 2417/2417 core unit tests pass (+2 regression), 380/380 web, 134/134 channels, 8/8 component demo stands (166/166 checks) * demo_crosslang T4: /tmp log confirms Node.js receives real Python payload ({"from":"python","marker":"run-..."}) for the first time — previously 8-byte empty-object marker only Refs: PRD-020, KNOWN-ISSUES.md #17, #18 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ra at 4223/6381 Motivation: demos passing ≠ demos validating. Audit of the 166/166 green Sprint-5 suite against the 4 PRD-020 fixes (#16/#17/#18/#19) showed 3 of them were NOT actually exercised by any demo: #17 settings sanitizer — demo_web used memory:// transport, so no real serializer ever ran on the INFO packet. The callable-in-settings hang reproducing the original bug was impossible on memory. #18 EVENT data field — demo_crosslang T4 asserted only that the Node handler fired; it passed with an explicit "payload empty — EVENT params/data gap" note. The marker was never required in the wire payload, so the test stayed green both before and after the fix. #19 client error code — demo_web asserted `status in (400, 401)`, which passed both the buggy 400 and the fixed 401. #16 DLQ instance — demo_channels passed `{"dead_lettering": {...}}` dict form, never the typed instance form the fix was about. Each fix is now covered by a test that measurably FAILS without the fix, verified via surgical counterfactual: revert the single source line, rerun the demo, observe failure, restore. Changes per bug: #17 — demo_comprehensive: new t9_wire_safety group registers CallableSettingsService (lambdas + object() in instance settings). Two scenarios per transport: single-broker start/stop and two-broker remote discovery over the actual wire (NATS/Redis/MQTT/AMQP/Kafka, plus memory+tcp). On NATS this adds 13 real data points across 7 transports where json/msgpack/cbor serializers actually encode the INFO packet. Counterfactual: revert node.py:ensure_local_node -> service.settings -> callable-settings-start hangs for the full 10s timeout, exactly matching the original bug. #18 — demo_crosslang T4 assertion tightened. Old logic accepted "handler fired; payload empty" as PASS; new logic requires both handler firing AND the full marker string present in the Node- written /tmp/crosslang_test_T4_*.log. Counterfactual: revert transit.send_event to forwarding context.marshall() verbatim -> T4 fails with "handler fired but marker missing — EVENT payload gap; log='PING {}'" — exactly the pre-fix wire state. #19 — demo_web: tightened test_custom_middleware -> test_client_error_401 with exact `status == 401`, not `in (400, 401)`. Added two new actions (users.forbidden_op raising code=403, users.missing_resource raising code=404) and corresponding test_client_error_403 / test_client_error_404 with exact status matches. Counterfactual: revert errors.py to unconditional BadRequestError -> all three fail with status=400. #16 — demo_channels: new test_dlq_typed_options case. Constructs DeadLetteringOptions(enabled=True, ...) and RedisOptions(...) instances directly (not dicts), passes them through service schema, then after broker.start() looks up the parsed Channel in channel_registry and asserts identity (`channel.dead_lettering is typed_dlq`) — not dict-roundtrip equality. Counterfactual: revert middleware.py to dict-only branch -> test fails with "DeadLetteringOptions instance dropped (dict-roundtrip)". Infrastructure consolidation: docker-compose.yml moved into the moleculerpy repo (was floating in the parent workspace dir, which is not under version control — fresh clones would lose it entirely). The compose now publishes NATS on 4223 and Redis on 6381 instead of the usual 4222/6379, so MoleculerPy demos do not collide with other locally running NATS instances (e.g. graphrag-nats on 4222 which triggered this whole investigation when its volume went read-only mid-session). Ports chosen to match the long-standing "demo-valkey" convention (6381) and picked 4223 as the least-collisiony nearby free slot. Monitoring port similarly shifted 8222 -> 8223. All demo files updated to match: demo_channels.py:NATS_PORT -> 4223 demo_comprehensive.py:Transport("nats", ...) -> :4223 demo_matrix.py:Transport("nats", ...) -> :4223 demo_crosslang.py:NATS_PORT -> 4223, subprocess env now exports NATS_URL so node_services/index.js picks up the same endpoint. demo_repl.py:NATS_URL -> :4223, error hint rewritten. node_services/index.js reads NATS_URL from process.env (default "nats://localhost:4222" for standalone Node experiments) so Python drivers that set NATS_URL=nats://localhost:4223 are picked up. run_all_demos.py expected counts updated: demo_comprehensive 90/90 -> 103/103 (+13 from t9_wire_safety) demo_channels 6/6 -> 7/7 (+1 from dlq_typed_options) demo_web 11/11 -> 13/13 (+2 for 403 + 404 scenarios; 7th was tightened in-place) Evidence: * 8/8 demos | 182/182 tests | 148s on isolated moleculerpy-nats:4223 + moleculerpy-redis:6381 (zero dependence on graphrag-nats) * Counterfactual confirmed per bug: each fix's revert makes the corresponding new/tightened demo measurably fail. Full evidence trail in the commit that precedes this one (44811f5). * Unit suites unchanged: core 2417/2417, web 380/380, channels 134/134. Refs: PRD-020, KNOWN-ISSUES.md #16, #17, #18, #19 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Closes the last unanswered PRD-020 capability gap: until now the
project claimed "any Moleculer-v4 language can talk to MoleculerPy"
but only the CORE protocol (actions, events, discovery, lifecycle)
was actually verified cross-language via demo_crosslang. The
SEPARATE channels wire (raw JetStream streams + durable consumers
with their own envelope) had no automated interop proof — a Node.js
channels_interop.js scaffold had been committed to tests/ months ago
and abandoned without a Python counterpart.
This demo closes the gap on the only level that matters for the
user question ("can my Python + Node apps interoperate through
channels?"): the JetStream wire contract. Two bidirectional tests:
T1. Python → Node
moleculerpy-channels NatsAdapter.publish on "payments.completed"
→ Node consumer on the same subject deserializes and writes a
JSONL log. Python asserts the per-run marker is present with
full payload intact.
T2. Node → Python
Python asks Node over a core NATS request subject to publish
on "orders.created" with a per-run marker. Node publishes via
the raw nats.js JetStream client. Python's moleculerpy-channels
consumer fires its handler and records the payload. Assertion
requires the marker to round-trip.
Why a "direct nats.js" harness, not @moleculer/channels
-------------------------------------------------------
Building the demo surfaced an upstream regression in
@moleculer/channels 0.2.0 when paired with the current nats.js
2.29.x client: manager.streams.add() calls return did_create: true
in debug logs but the streams never actually land on the NATS
server, and the subsequent subscribe() silently registers zero
consumers (verified via a separate nats client connection to the
same broker showing 0 consumers against the claimed streams on both
NATS 2.10.29 and 2.12.6). moleculerpy-channels' own NATS adapter is
NOT affected (demo_channels 7/7 passes end-to-end against the same
broker), so the defect is strictly on the Node.js high-level
wrapper, not the wire protocol itself.
The correct level to prove cross-language compatibility is the
JetStream wire — if both sides agree on stream naming convention
(dots → underscores), publish subject, and JSON envelope, they
interop regardless of which middleware they layer on top. The new
channels_interop_direct.js Node harness uses the raw `nats` package
(which demonstrably works) to subscribe/publish on the same subjects
a @moleculer/channels (or any other) library would use. Any future
fixed @moleculer/channels release will interoperate the same way
because the wire contract is identical.
The old @moleculer/channels-based channels_interop.js is left in
place for reference but no longer wired into the demo suite — its
first line is updated to read NATS_URL from env so it is still a
valid standalone exploration script when paired with a future
working @moleculer/channels.
Counterfactual verification
---------------------------
Proved this demo is a real regression guard, not a noisy always-green
test:
* Break the publish subject (NatsAdapter.publish → subject + ".BROKEN")
→ demo fails loudly with MessagePublishError "no response from stream".
* Corrupt the payload envelope (NatsAdapter.publish → payload =
b'{"corrupted":true}') → T1 fails with "marker not observed",
T2 still passes (Node→Python path untouched), proving the assertion
discriminates "delivered but corrupt" from "delivered intact".
Infrastructure change
---------------------
docker-compose.yml pinned from nats:2-alpine to nats:2.10-alpine.
NATS 2.12 enables JetStream strict mode and API level 3 which the
cross-language investigation flagged as a potential @moleculer/channels
compat hazard. moleculerpy-channels works fine on 2.12+, but the pin
gives us a stable baseline for any future Node.js channels interop
work. Documented inline in docker-compose.yml.
Evidence
--------
* 9/9 demos | 184/184 tests | 146s on dedicated moleculerpy-nats:4223
+ moleculerpy-redis:6381 (demo_crosslang_channels adds 2/2)
* Counterfactual per test case confirmed (subject break + envelope
corrupt) — see above.
* Core unit suites unchanged: 2417/2417 core, 380/380 web, 134/134
channels.
* tests/integration/node_services/index.js and
channels_interop.js both read NATS_URL from env so cross-language
tests all flow through the compose file's single endpoint.
Refs: PRD-020, KNOWN-ISSUES.md (cross-lang channels was previously
unverified, now proven at the wire-contract level).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…gression Release prep for v0.14.22 (PRD-020 bug closure sprint). Two kinds of changes in one commit, kept together because they cannot ship apart: 1. Version bump — pyproject.toml, moleculerpy/__init__.py fallback, README "Current status" header, and a comprehensive CHANGELOG entry documenting the #17/#18 fixes, the new demo coverage (comprehensive T9, tightened crosslang T4, new crosslang_channels), the counterfactual evidence, and the infrastructure changes (compose relocation, port choices, NATS 2.10 pin). 2. **P0 packaging fix** — smoke install step caught that `moleculerpy/cacher/__init__.py` was unconditionally doing `from .redis import RedisCacher`, but `redis` is only declared in the `test` extra of pyproject.toml, not in the core dependencies. As a result, a plain `pip install moleculerpy` would crash on ANY ServiceBroker construction with `ModuleNotFoundError: No module named 'redis'`. This regression has been latent since 0.14.10 (commit e2fa6b8 introduced the unconditional import) and was only caught now because the pipeline's smoke-install step runs in a clean venv with only the declared base dependencies — exactly what a PyPI user gets. Fix mirrors the pattern already used by moleculerpy-channels/adapters: try: from .redis import RedisCacher _HAS_REDIS_CACHER = True except ImportError: _HAS_REDIS_CACHER = False RedisCacher = None # type: ignore and the "redis" / "Redis" entries are only added to _CACHER_REGISTRY when _HAS_REDIS_CACHER is True. `resolve("redis")` on a base install now fails loudly with the informative "Unknown cacher type: 'redis'" instead of crashing earlier at import time. Fixing this was not in the original PRD-020 scope but the sprint goal is "make v0.14.22 releasable" and the smoke step is a quality gate — an un-importable package cannot ship regardless of whether its symptom is PRD-020-related. Evidence post-change: * Full core unit suite: 2417/2417 pass (1 skipped, pre-existing) * Demo suite: 9/9 | 184/184 | 149s on dedicated moleculerpy-nats:4223 + moleculerpy-redis:6381 * Smoke install in /tmp/mpy-smoke-0.14.22 (python 3.12.7, only nats-py + structlog + psutil installed): - moleculerpy 0.14.22 - moleculerpy-web 0.1.1 - moleculerpy-channels 0.2.1 ServiceBroker, ChannelsMiddleware, ApiGatewayService, and all 401/403/404 error classes import cleanly; ``_serializable_settings``, ``Lifecycle.rebuild_event_context``, and ``Transit.send_event``'s ``broadcast: bool = False`` kwarg all present and typed correctly; ``resolve("memory")`` works, ``resolve("redis")`` raises ``ValueError: Unknown cacher type: 'redis'`` as expected without the redis package. Refs: PRD-020, KNOWN-ISSUES.md #16 #17 #18 #19 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…e sanitiser + NaN probe Addresses the HIGH/CRITICAL findings from the 2-agent pre-release audit of the PRD-020 v0.14.22 branch: ## CRITICAL — `send_event_with_ack` bypassed the EVENT wire schema fix Wire auditor (sonnet, audit run 2026-04-09) flagged ``moleculerpy/transit.py:1502`` as CRITICAL. Before this commit, ``Transit.send_event_with_ack`` published via ``self.publish(Packet(Topic.EVENT, ..., context.marshall()))`` — ``context.marshall()`` serialises the event payload under the legacy ``params`` key, which bypassed the KNOWN-ISSUES #18 fix entirely for the reliable-event (needAck) code path. Any event sent with ``needAck=True`` to a Node.js peer would arrive with ``ctx.data === undefined``. The reliable-event path had no production caller yet (no ``broker.emit_with_ack`` surface exposed), but it IS a public ``Transit`` API and was exercised by ``event_ack_test.py`` — the regression would have landed silently the moment the broker-level API was added later. Fix: ``send_event_with_ack`` now delegates to ``send_event`` instead of building its own publish. ``send_event`` was already updated in 44811f5 with the Node.js-parity wire payload, so the ACK path inherits all of ``data``, ``broadcast``, ``groups``, ``needAck``, ``caller``, ``parentID``, ``requestID``, ``meta``, ``level``, ``tracing``. The ``broadcast=False`` default is correct for ACK paths (they are always directed to a specific endpoint, never multicast). Regression guard: ``test_bug17_send_event_with_ack_uses_data_field`` in ``tests/unit/audit_regression_test.py`` does both a static source probe (``"self.send_event(" in source``) AND a functional probe constructing a real ``Transit``, calling ``send_event_with_ack`` against a mock transporter, and asserting the captured packet has ``data`` but not ``params``, plus ``needAck`` is True. A future regression that inlines the publish back would fail both probes. ## HIGH — ``_serializable_settings`` permitted NaN/inf via json.dumps(allow_nan=True) Wire auditor flagged ``moleculerpy/node.py:67`` as HIGH. Python's ``json.dumps`` defaults to ``allow_nan=True``, so ``float('nan')``, ``float('inf')``, and ``float('-inf')`` pass the probe as the literal strings ``"NaN"`` / ``"Infinity"`` — which are not valid JSON per RFC 8259 §6 and which MsgPack's ``msgpack.packb`` rejects with ``PackException``. A service with a NaN in its settings would have crashed the INFO packet mid-handshake on NATS + MsgPack the moment it reached any non-JSON transporter. Fix: new ``_is_wire_scalar`` helper uses ``json.dumps(value, allow_nan=False)`` as the probe. NaN/inf now fail the probe and get stripped alongside callables. The change also rejects non-scalar leaves at arbitrary depth, which feeds into the next finding. ## HIGH — ``_serializable_settings`` top-level drop was too coarse Wire auditor flagged the old behaviour where a nested structure like ``{"routes": [{"path": "/api", "hook": <callable>}]}`` would fail the top-level probe because ``json.dumps`` recurses into the list and chokes on the callable — causing the *entire* ``routes`` list to be dropped, including the ``path``/``method``/``aliases`` structure that remote nodes legitimately want to see. Losing the whole ``routes`` config defeats the point of shipping settings over the wire. Fix: ``_sanitize_wire_value`` is a new recursive walker that preserves dict / list shape and only strips the actual bad leaves. A ``{"routes": [{"path": "/api", "hook": <callable>}]}`` input now becomes ``{"routes": [{"path": "/api"}]}``. Non-serialisable dict keys are dropped too. The WARNING message now says "affected top-level keys" rather than "dropped keys" to reflect the granular sub-rewriting, and adds a hint that peer services should receive runtime values via action params or metadata, not settings. Regression guards: - ``test_bug17_rejects_nan_and_inf_for_binary_serializer_safety`` — NaN/inf stripped, strict JSON round-trip. - ``test_bug17_recursive_sanitisation_preserves_siblings`` — a ``routes`` list with one good entry and one entry containing nested callables round-trips with ``path``/``method``/``aliases`` preserved and the callables removed. The good sibling entry (``/health``) survives intact. - ``test_bug17_service_settings_with_callables_are_stripped`` tightened to assert the exact surviving key set (``{"port", "host", "routes"}``) per demo auditor recommendation — guards against a future regression that would over-zealously drop safe sibling keys. ## MEDIUM — ``ServiceNotFoundError`` does NOT shadow ## ``MoleculerClientError(code=404)`` in the gateway Demo auditor verified that the 404 branch in ``demo_web`` is reached cleanly: ``ServiceNotFoundError`` inherits from ``MoleculerRetryableError``, not ``MoleculerClientError``, so an explicit ``MoleculerClientError(code=404)`` falls through to the new numeric code branch and is correctly mapped to HTTP 404. No fix needed — this is a positive observation. ## LOW — old ``channels_interop.js`` stale and unreferenced Demo auditor noted that ``tests/integration/node_services/channels_interop.js`` was no longer referenced by any demo after 8a1fbf6 introduced ``channels_interop_direct.js``. Leaving both files risks confusing future contributors about which harness is the active one. Deleted ``channels_interop.js`` (the old ``@moleculer/channels`` reference implementation — now known to have the upstream stream-creation regression documented in 8a1fbf6). The ``channels_interop_direct.js`` file that bypasses ``@moleculer/channels`` is the only interop harness going forward until an upstream fix ships. ## Evidence after all audit fixes - Core unit: 2420/2420 pass (+3 new regression tests from this commit), 1 skipped pre-existing. - Channels unit: 135/135 (+1 new ``test_parse_channel_accepts_redis_options_instance`` mirror test per demo auditor recommendation, committed separately in the channels repo). - Demo suite: 9/9 | 184/184 | 157s on moleculerpy-nats:4223 + moleculerpy-redis:6381. - Smoke install in /tmp/mpy-smoke-0.14.22 still clean (no change to the cacher optional-import fix). - Lint / format / mypy: all pass on touched files. Refs: PRD-020, wire-audit 2026-04-09, demo-coverage-audit 2026-04-09 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Resolves version-field conflicts in pyproject.toml and moleculerpy/__init__.py. Dev had an independent 0.14.19 → 0.14.21 bump path through commits aaa1719 and 201ca64, while this branch already carries the 0.14.21 → 0.14.22 bump in d4b0b6b. Resolution: take 0.14.22 (HEAD) everywhere since it is strictly ahead of dev's 0.14.21 on both version strings. Post-merge checks: * ruff / mypy / unit suite (2420 passed) all still green * demo_crosslang sanity smoke 5/5 Refs: PRD-020 release merge prep for v0.14.22
[PRD-020] v0.14.22 bug closure sprint — cross-lang wire schema + settings sanitiser
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release v0.14.22 — PRD-020 bug closure sprint
Brings dev → main for the v0.14.22 release. Contains the full PRD-020 bug-closure sprint merged via #46 plus the protocol lifecycle / heartbeat revert work merged via #44.
Shipped in this release
_serializable_settingsrecursive sanitiser strips callables / non-JSON leaves while preserving valid structure.allow_nan=Falseprobe rejects NaN/inf.paramstransit.send_eventnow builds Node.js-parity wire payload (datafield +broadcast+groups+needAck+caller). NewLifecycle.rebuild_event_contextfor receive-side translation.send_event_with_ackalso uses the new path (audit-caught regression).moleculerpy/cacher/__init__.pyno longer hard-requiresredis— it's only in thetestextra. Cleanpip install moleculerpyno longer crashes onServiceBroker()construction.Evidence summary
demo_crosslang5/5 +demo_crosslang_channels2/2d751d0bTest plan
pip install moleculerpy==0.14.22in clean venv + import smokePost-merge
git push origin v0.14.22→ triggers.github/workflows/publish.yamltest → build → publish → GitHub releasepipelinemoleculerpy 0.14.22)git checkout dev && git merge main && git push origin devRefs: PRD-020, KNOWN-ISSUES.md #16/#17/#18/#19
🤖 Generated with Claude Code