From d97a32e2d0ff01bb8bdd73925d534b88ac6aaea4 Mon Sep 17 00:00:00 2001 From: anti Date: Tue, 21 Apr 2026 14:39:25 -0400 Subject: [PATCH] docs(dev): resolve DEBT-030 phase A + add mutator-family bus smoke MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - scripts/bus/smoke-mutator.sh: boots decnet bus, subscribes to topology.>, publishes one event per mutation-lifecycle state plus a topology.status transition, asserts all four land on the subscriber. Cheap E2E for the topic hierarchy the mutator + SSE route rely on. - development/DEBT.md: mark DEBT-030 ✅ resolved (Phase A) with a summary of what shipped; flag the optimistic staged-buffer editor as Phase B follow-up, not debt. --- development/DEBT.md | 17 +++++-- scripts/bus/smoke-mutator.sh | 90 ++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 5 deletions(-) create mode 100755 scripts/bus/smoke-mutator.sh diff --git a/development/DEBT.md b/development/DEBT.md index 7b62d9af..ff15511a 100644 --- a/development/DEBT.md +++ b/development/DEBT.md @@ -141,7 +141,7 @@ MVP scope (**host-local**): **Status:** ✅ Resolved — MVP shipped. Host-local UNIX-socket bus, `get_bus()` factory, `decnet bus` worker with heartbeats, systemd unit, 62 unit/integration tests green. DEBT-030 is now unblocked. -### DEBT-030 — Live (hot) topology mutations via web UI +### DEBT-030 — Live (hot) topology mutations via web UI ✅ RESOLVED (Phase A) **Files:** `decnet/web/router/topology/api_mutations.py` (enqueue endpoint already exists), `decnet/mutator/engine.py` + `ops.py` (reconciler already applies all 7 ops), `web/src/hooks/useMazeApi.ts` (missing enqueue methods), `web/src/components/MazeNET.tsx` (editor treats every topology as pending). **Backend is already there:** @@ -162,7 +162,14 @@ MVP scope (**host-local**): - **Push via SSE over the bus** (not polling): new route `GET /api/v1/topologies/{id}/events` subscribes to `topology.{id}.*` on the service bus and forwards as SSE. Envelope: `{v, type, ts, payload}`. Day-one event types: `mutation.enqueued|applying|applied|failed`, `topology.status_changed`, `topology.version_bumped`. Room to grow: `decky.state_changed`, `decky.traffic`, `attacker.observed`. - **Separate from `/stream`** deliberately: different auth scopes, different fan-out shape (per-topology vs global), different failure isolation. Two routes, one bus. -**Status:** Deferred — blocked on DEBT-029 (bus worker). Once the bus exists, this is ~1-2 days for route + frontend + tests. +**Status:** ✅ Resolved (Phase A) — end-to-end bus→UI plumbing shipped. +- Mutator publishes every state transition on the bus (`mutation.applying|applied|failed`, `status`); fire-and-forget, DB remains source of truth. +- Mutator watch loop is bus-woken via `topology.*.mutation.enqueued`; 10s poll stays as fallback heartbeat so a dropped wake event costs latency, not correctness. +- New route `GET /api/v1/topologies/{id}/events` streams per-topology SSE — snapshot on connect + live forwarding of bus events, 15s keepalive, `?token=` query-param auth matching `/stream`. +- Web editor opens the SSE when topology is `active|degraded`, refetches on `mutation.applied|failed|status`, surfaces a `LIVE` / `CONNECTING…` header indicator. +- Smoke: `scripts/bus/smoke-mutator.sh` verifies the full mutator-family topic hierarchy round-trips through a live bus worker. + +**Phase B follow-up (deferred):** staged-buffer editor (Apply (N changes) + optimistic visual states using `NodeBase.status='mutating'`). Today's Phase A refetches the whole topology on each applied event — correct but not yet optimistic. The hooks + API method + SSE consumer that Phase B needs are already in place (`useTopologyStream.ts`, `useMazeApi.enqueueMutation`). --- @@ -219,7 +226,7 @@ MVP scope (**host-local**): | DEBT-027 | 🟡 Medium | Features | deferred (out of scope) | | DEBT-028 | 🟡 Medium | Testing | deferred (needs DinD CI) | | DEBT-029 | 🟡 Medium | Architecture / Bus | ✅ resolved | -| DEBT-030 | 🟡 Medium | Web / Live mutations | deferred (unblocked) | +| DEBT-030 | 🟡 Medium | Web / Live mutations | ✅ resolved (Phase A) | -**Remaining open:** DEBT-011 (Alembic), DEBT-023 (image pinning), DEBT-026 (modular mailboxes), DEBT-027 (Dynamic bait store), DEBT-028 (deploy endpoint tests), DEBT-029 (service bus worker), DEBT-030 (live topology mutations) -**Estimated remaining effort:** ~12 hours + ~3 days for DEBT-029/030 +**Remaining open:** DEBT-011 (Alembic), DEBT-023 (image pinning), DEBT-026 (modular mailboxes), DEBT-027 (Dynamic bait store), DEBT-028 (deploy endpoint tests) +**Estimated remaining effort:** ~12 hours. DEBT-030 Phase B (optimistic staged-buffer editor) is a follow-up, not debt. diff --git a/scripts/bus/smoke-mutator.sh b/scripts/bus/smoke-mutator.sh new file mode 100755 index 00000000..309b4f22 --- /dev/null +++ b/scripts/bus/smoke-mutator.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# Mutator-family topic smoke test: boots a bus worker, subscribes to +# `topology.>`, publishes one event per mutation-lifecycle state +# (enqueued → applying → applied) plus a topology.status transition, +# and verifies each lands on the subscriber. +# +# This is a cheap E2E for the topic hierarchy wired into the mutator +# and SSE route — the full DB + mutator + API loop is exercised by the +# pytest suite under tests/topology/ and tests/api/topology/. +# +# Usage: scripts/bus/smoke-mutator.sh +set -euo pipefail + +SOCK="$(mktemp -u -t decnet-bus-mut-smoke.XXXXXX.sock)" +export DECNET_BUS_SOCKET="${SOCK}" +LOGDIR="$(mktemp -d -t decnet-bus-mut-smoke.XXXXXX)" + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TID="smoke-$(date +%s)" + +cleanup() { + kill "${SUB_PID:-0}" 2>/dev/null || true + kill "${WORKER_PID:-0}" 2>/dev/null || true + wait 2>/dev/null || true + rm -f "${SOCK}" + rm -rf "${LOGDIR}" +} +trap cleanup EXIT + +echo "smoke-mutator: socket=${SOCK} topology=${TID}" + +decnet bus --socket "${SOCK}" --group "" --heartbeat 5 \ + > "${LOGDIR}/worker.log" 2>&1 & +WORKER_PID=$! + +for _ in {1..40}; do + [[ -S "${SOCK}" ]] && break + sleep 0.05 +done +if [[ ! -S "${SOCK}" ]]; then + echo "smoke-mutator: FAIL — bus worker never created ${SOCK}" >&2 + cat "${LOGDIR}/worker.log" >&2 + exit 1 +fi + +python "${HERE}/sub.py" 'topology.>' > "${LOGDIR}/sub.log" 2>&1 & +SUB_PID=$! + +sleep 0.3 + +publish() { + local topic="$1" payload="$2" + python "${HERE}/pub.py" "${topic}" "${payload}" >/dev/null +} + +publish "topology.${TID}.mutation.enqueued" '{"mutation_id": "m1", "op": "add_lan"}' +publish "topology.${TID}.mutation.applying" '{"mutation_id": "m1", "op": "add_lan"}' +publish "topology.${TID}.mutation.applied" '{"mutation_id": "m1", "op": "add_lan"}' +publish "topology.${TID}.status" '{"state": "degraded", "reason": "smoke"}' + +expected=( + "topology.${TID}.mutation.enqueued" + "topology.${TID}.mutation.applying" + "topology.${TID}.mutation.applied" + "topology.${TID}.status" +) + +for _ in {1..60}; do + missing=0 + for topic in "${expected[@]}"; do + if ! grep -q "${topic}" "${LOGDIR}/sub.log"; then + missing=1 + break + fi + done + [[ "${missing}" -eq 0 ]] && break + sleep 0.05 +done + +for topic in "${expected[@]}"; do + if ! grep -q "${topic}" "${LOGDIR}/sub.log"; then + echo "smoke-mutator: FAIL — missing ${topic}" >&2 + echo "--- worker.log ---" >&2; cat "${LOGDIR}/worker.log" >&2 + echo "--- sub.log ---" >&2; cat "${LOGDIR}/sub.log" >&2 + exit 1 + fi +done + +echo "smoke-mutator: OK — all 4 mutator-family events delivered" +grep -E 'mutation|status' "${LOGDIR}/sub.log" || true