From c69b0f7ca1d4e472d50e73c0d57c4148a71aecda Mon Sep 17 00:00:00 2001 From: jedarden Date: Mon, 6 Jul 2026 13:13:28 -0400 Subject: [PATCH] =?UTF-8?q?test(bf-95tx):=20integration=20test=20tracing?= =?UTF-8?q?=20positions=20through=20simulator=E2=86=92registry=E2=86=92fus?= =?UTF-8?q?ion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bf-95tx wants a single demonstrable verification that node positions flow through every runtime stage the position-propagation work touched. bf-69ym locked in simulator→fleet-registry; bf-u7ds locked in simulator→fusion. This adds the missing end-to-end lock-in that ties both together in one run. TestPositionPropagation_FullPipeline wires the production chain behind the SimulatorHandler (handler → bridge.SyncToRegistry → adapter → fleet.Registry SQLite, with adapter forwardPos → fleet.Manager.ForwardNodePosition → sink → fusion.Engine.SetNodePosition), posts one node with an explicit position, then reads that SAME position back from all three stages and asserts they agree — at both AddNode and UpdateNode. A pipelineStages helper logs each stage's value so `go test -v` visibly traces the value's path: [create] position propagation simulator={1.5 2.5 1} registry={1.5 2.5 1} fusion={1.5 2.5 1} [update] position propagation simulator={3.5 4.5 2} registry={3.5 4.5 2} fusion={3.5 4.5 2} The update leg also asserts no stage still holds the stale pre-update value and that a single node stays a single node at every stage (no orphan/dup). Co-Authored-By: Claude --- mothership/internal/api/simulator_test.go | 122 ++++++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/mothership/internal/api/simulator_test.go b/mothership/internal/api/simulator_test.go index 5381150..f1dd87e 100644 --- a/mothership/internal/api/simulator_test.go +++ b/mothership/internal/api/simulator_test.go @@ -658,3 +658,125 @@ func TestSimulatorHandlerToRegistry_OriginNodesGetSpreadGeometry(t *testing.T) { } } } + +// TestPositionPropagation_FullPipeline (bf-95tx) is the end-to-end lock-in for +// the COMPLETE simulator → fleet registry → fusion engine position path in a +// single test. One node position, posted once through the simulator REST API, +// must arrive — unchanged and in order — at every stage the runtime cares about: +// +// 1. simulator VirtualNodeStore — the source of truth for virtual nodes +// 2. fleet.Registry (SQLite) — persisted; read by the dashboard/REST +// 3. fusion.Engine nodePos mirror — what Fuse localizes against at runtime +// +// bf-69ym locked in stages 1→2; bf-u7ds locked in stages 1→3. This test ties +// them together: it traces ONE position value through all three stages in a +// single run and asserts they agree at both AddNode and UpdateNode, with +// per-stage logging so the full data path is observable under `go test -v`. +// That is the demonstrable full-pipeline verification the position-propagation +// bead (bf-95tx) requires. +func TestPositionPropagation_FullPipeline(t *testing.T) { + // Wire the full chain exactly as cmd/mothership/main.go does: + // handler → bridge.SyncToRegistry → adapter → fleet.Registry (SQLite), + // with the adapter's forwardPos → fleet.Manager.ForwardNodePosition → + // nodePositionSink → fusion.Engine.SetNodePosition. + reg := newFleetRegistry(t) + mgr := fleet.NewManager(reg) + engine := fusion.NewEngine(nil) + mgr.SetNodePositionSink(func(mac string, x, y, z float64) { + engine.SetNodePosition(mac, x, y, z) + }) + adapter := &fleetRegistryTestAdapter{ + reg: reg, + forwardPos: mgr.ForwardNodePosition, + } + h := newWiredHandler(t, adapter) // real VirtualNodeStore + bridge + adapter + + // pipelineStages reads the posted node's position back from all three stages + // and logs each, so a `go test -v` run visibly traces the value's path: + // simulator → registry → fusion. nodeID keys the simulator store; name keys + // the fleet registry row (the bridge's virtualMAC is unexported); the single + // positioned node keys the fusion engine mirror. + pipelineStages := func(label, nodeID, name string) (simPos, regPos, fusPos simulator.Point) { + t.Helper() + + // Stage 1 — simulator VirtualNodeStore (the source). + vs, err := h.virtualStore.GetNode(nodeID) + if err != nil { + t.Fatalf("%s: simulator store lookup %q: %v", label, nodeID, err) + } + simPos = simulator.Point{X: vs.Position.X, Y: vs.Position.Y, Z: vs.Position.Z} + + // Stage 2 — fleet.Registry (SQLite), matched by Name. + byName := registryNodesByName(t, reg) + rec, ok := byName[name] + if !ok { + t.Fatalf("%s: node %q missing from fleet registry (have %d nodes)", label, name, len(byName)) + } + regPos = simulator.Point{X: rec.PosX, Y: rec.PosY, Z: rec.PosZ} + + // Stage 3 — fusion.Engine nodePos mirror (what Fuse localizes against). + fusPos = singleEnginePosition(t, engine) + + t.Logf("[%s] position propagation simulator=%v registry=%v fusion=%v", + label, simPos, regPos, fusPos) + return simPos, regPos, fusPos + } + + // --- Create leg: post one node with an explicit position. --- + const nodeID, nodeName = "node-A", "Node A" + createPos := simulator.Point{X: 1.5, Y: 2.5, Z: 1.0} + postNode(t, h, map[string]interface{}{ + "id": nodeID, + "name": nodeName, + "position": map[string]interface{}{ + "x": createPos.X, "y": createPos.Y, "z": createPos.Z, + }, + }) + + simPos, regPos, fusPos := pipelineStages("create", nodeID, nodeName) + if simPos != createPos { + t.Errorf("create: simulator stage %v != posted %v", simPos, createPos) + } + if regPos != createPos { + t.Errorf("create: registry stage %v != posted %v", regPos, createPos) + } + if fusPos != createPos { + t.Errorf("create: fusion stage %v != posted %v", fusPos, createPos) + } + + // --- Update leg: move the node and re-trace the full pipeline. --- + updatePos := simulator.Point{X: 3.5, Y: 4.5, Z: 2.0} + putNode(t, h, nodeID, map[string]interface{}{ + "id": nodeID, + "name": nodeName, + "position": map[string]interface{}{ + "x": updatePos.X, "y": updatePos.Y, "z": updatePos.Z, + }, + }) + + simPos, regPos, fusPos = pipelineStages("update", nodeID, nodeName) + if simPos != updatePos { + t.Errorf("update: simulator stage %v != posted %v", simPos, updatePos) + } + if regPos != updatePos { + t.Errorf("update: registry stage %v != posted %v", regPos, updatePos) + } + if fusPos != updatePos { + t.Errorf("update: fusion stage %v != posted %v", fusPos, updatePos) + } + + // No stage may still hold the pre-update value after the position moved. + if simPos == createPos || regPos == createPos || fusPos == createPos { + t.Errorf("update: a stage still holds the stale pre-update position %v "+ + "(sim=%v reg=%v fus=%v)", createPos, simPos, regPos, fusPos) + } + + // Cross-stage consistency: a single node must stay a single node at every + // stage — an update must not orphan or duplicate it anywhere in the path. + if got := len(registryNodesByName(t, reg)); got != 1 { + t.Errorf("update: expected 1 fleet registry node, got %d", got) + } + if got := engine.NodeCount(); got != 1 { + t.Errorf("update: expected 1 fusion engine node, got %d", got) + } +}