Commit graph

871 commits

Author SHA1 Message Date
jedarden
1df0bfaa21 test(fusion): assert default-placement produces non-zero peaks (bf-1kid)
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
The bf-18yn umbrella's acceptance-criteria test. Using ONLY the default
node placement (simulator.DefaultNodePositions — the spread geometry a
freshly-onboarded virtual/sim fleet receives with no manual positioning,
bf-3fr6/bf-xrej), seed the 3D fusion engine and assert the accumulation
grid produces non-zero fusion peaks, closing the bf-4q5w symptom (the
engine emitting no / degenerate peaks).

TestEngine_DefaultPlacementProducesPeaks is the fleet->engine counterpart
to TestEngine_SeedNodePositions (bf-6s3d): bf-6s3d locks in the seeding
invariant (distinct, non-(0,0,1) positions); this locks in the downstream
consequence the seeding exists to deliver — that spread nodes actually
let Fuse form blobs. It runs over default placements of 2/3/4/6 nodes,
drives a synthetic walker through the room centre, and asserts
len(blobs) > 0 OR gridMax above threshold. assertPlacementNotCollapsed
fails loudly if default placement ever collapses to (0,0,1).

TestEngine_CoLocatedOriginYieldsNoPeaks is the counter-example pinning
bf-4q5w: nodes left at the (0,0,1) DB default are co-located, every link
is degenerate, the grid stays at zero, and Fuse emits no blobs — proving
the non-zero-peak assertion is meaningful, not trivially satisfiable.

Acceptance: passes with default placement; co-located (0,0,1) collapse
yields no peaks (documented by the counter-example). go build ./... /
go vet ./... / go test ./... all pass across mothership, cmd/sim, and
test/acceptance.

Co-Authored-By: Claude <noreply@anthropic.com>
Bead-Id: bf-1kid
2026-07-03 19:18:46 -04:00
jedarden
307ab0e194 feat(simulator): seed spread geometry for default virtual nodes in registry bridge (bf-xrej)
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
Virtual nodes created without an explicit position carry the nodes-table DB
defaults (pos_x=0, pos_y=0, pos_z=1), so SyncToRegistry/SyncOneNode wrote
co-located origin positions into the fleet registry — and downstream into the
fusion engine — collapsing Fresnel excess paths (core symptom in bf-18yn).

Resolve positions through a new effectivePositions helper: nodes still at the
default origin are reassigned distinct, spread-out geometry from
DefaultNodePositions (sized to the full node count, across the store's space),
while explicitly-placed nodes keep their position. Slot assignment is keyed by
sorted node ID so it is deterministic regardless of map iteration order, and a
single-node sync writes the same position a full sync would. The effective
positions flow through SyncToRegistry, SyncOneNode, and ToRegistryRecords, so
the registry and fusion-engine seeding observe non-degenerate geometry.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 19:00:37 -04:00
jedarden
d048987d61 feat(ingestion): persist hello-announced node position in fleet registry (bf-24xp)
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
spaxel-sim computed good corner/perimeter geometry in createVirtualNodes but
the hello message sent on WebSocket connect did not include position, so the
mothership never learned the sim node's location and the fleet registry/DB row
was left at the nodes-table schema default (0,0,1) — co-located with every
other node.

Both sim CLIs (cmd/sim and mothership/cmd/sim) now announce pos_x/pos_y/pos_z
in the hello handshake, sourced from createVirtualNodes. HelloMessage gains
PosX/PosY/PosZ as *float64 (not plain float64) so an absent position is
distinguishable from a genuine (0,0,0): a real ESP32 omits these in hello
(its position is user-placed in the dashboard), whereas spaxel-sim announces
its computed geometry.

The connect/register path (server.HandleNodeWS -> FleetNotifier.OnNodeConnected
-> fleet.Manager) now carries the announced position through. The Manager
persists it via registry.SetNodePosition only when all three axes are present,
so a nil/partial announcement preserves any existing user-placed position
rather than clobbering it. The position then flows onward to the fusion engine
through the bf-3p6g connect/register sink (ForwardNodePosition ->
nodePositionSink -> fusionEngine.SetNodePosition), so a freshly connected sim
node is seeded at its announced location instead of (0,0,1).

The FleetNotifier interface change (OnNodeConnected gains three *float64
params) is propagated to every implementer — Manager (persists), FleetHealer
and SelfHealManager (read geometry from the registry, ignore), the guided-
troubleshoot notifier (tracks connect/disconnect only), and the cmd/mothership
multiFleetNotifier fan-out.

Tests: existing OnNodeConnected call sites updated to the new signature.
TestManagerOnNodeConnectedPersistsHelloPosition asserts a node connecting with
an announced position yields a registry row whose pos differs from (0,0,1) and
matches the sent coordinates, and that the same coordinates reach the fusion
engine. TestManagerOnNodeConnectedWithoutHelloPositionPreservesExisting
asserts the real-ESP32 case (nil announce) and partial-announce both leave an
existing user-placed position untouched.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 18:42:34 -04:00
jedarden
3d53616594 Merge remote-tracking branch 'origin/main'
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
2026-07-03 18:30:34 -04:00
jedarden
eb4ad31aae Merge remote-tracking branch 'origin/main' 2026-07-03 18:29:36 -04:00
jedarden
1f3e39a6c5 feat(simulator): add shared DefaultNodePositions helper for spread node geometry (bf-3fr6)
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
2026-07-03 18:18:39 -04:00
jedarden
a17bdd2c8c feat(fusion): propagate runtime node positions to the fusion engine (bf-3p6g)
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
Forward node 3D position changes to the blob-producing fusion engine so a
node moved at runtime does not keep a stale engine position. The fleet
manager gains a nodePositionSink — the write-side mirror of the read-side
SetNodePositionAccessor used by diagnostics and weather.go — wired to
fusionEngine.SetNodePosition at startup. It fires from both position paths:
PATCH /api/nodes/{mac}/position (handler.updateNodePosition →
ForwardNodePosition) and node connect/register (OnNodeConnected forwards the
registry position). Tests assert the engine's stored position for a MAC
changes after a PATCH with new coordinates and is seeded on connect.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 17:31:40 -04:00
jedarden
3785619967 feat(fusion): assert engine node positions at startup (bf-1tsm)
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
bf-6s3d added TestEngine_SeedNodePositions, the host/unit-test half of
the node-position invariant (it replays main.go's seeding loop against
the engine and asserts NodeCount()/NodePositions() hold distinct,
non-(0,0,1) positions). This bead adds the complementary half the
NodePositions() docstring (fusion.go:140-159) already advertises:
"a logged startup assertion" exercised in the live path.

Right after the main.go seeding loop (which calls
fusionEngine.SetNodePosition(node.MAC, node.PosX, node.PosY, node.PosZ)
per fleet node), the engine now inspects NodePositions() and logs:

- n == 0          → INFO  "no nodes positioned yet" (valid; IO-1 reaches
                              ready with no nodes attached)
- atDefault == n  → WARN  "all N seeded nodes collapsed to (0,0,1)"
                             (fleet never positioned → degenerate fusion)
- otherwise       → INFO  "N nodes seeded, D distinct, A at (0,0,1)"

The (0,0,1) collapse is the failure mode: when no node has been
positioned, every node retains the nodes-table schema default
(pos_x=0, pos_y=0, pos_z=1) and Fuse emits only degenerate peaks. The
assertion surfaces this without blocking startup (the hard gate stays
the bf-6s3d unit test).

Also gofmt's the file (drive-by): bf-3f6q's "fusion" import sat after
"ingestion" instead of alphabetically after "floorplan".

go build ./... / go vet ./... / go test ./... all pass; gofmt clean.

Co-Authored-By: Claude <noreply@anthropic.com>
Bead-Id: bf-1tsm
2026-07-03 16:54:15 -04:00
jedarden
ce86575df4 test(fusion): lock in fleet→engine node-seeding invariant (bf-6s3d)
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
The 3D fusion engine's startup node seeding (iterate
fleetReg.GetAllNodes() and call SetNodePosition(node.MAC, node.PosX,
node.PosY, node.PosZ) per node, reading the DB pos_x/pos_y/pos_z
columns) was already wired into the live path by bf-3f6q (main.go
seeding loop sits symmetrically with the selfImprovingLocalizer
seeding). This bead's contribution is a regression test codifying the
bf-6s3d acceptance criteria so a future refactor cannot silently
regress them:

- NodeCount() equals the number of fleet nodes returned by GetAllNodes()
- NodePositions() holds a distinct, non-(0,0,1) position for each
  registered node (nodes must NOT collapse to the co-located DB
  default of pos_x=0, pos_y=0, pos_z=1)

TestEngine_SeedNodePositions replays the main.go seeding pattern
against the engine and asserts all three invariants (count, exact
coordinate round-trip, distinctness, non-default).

go build ./... / go vet ./... / go test ./... all pass.

Co-Authored-By: Claude <noreply@anthropic.com>
Bead-Id: bf-6s3d
2026-07-03 16:42:09 -04:00
jedarden
30a7ea346e feat(fusion): construct 3D fusion engine in live path and feed the blob loop (bf-3f6q)
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
The 3D Fresnel fusion engine (internal/fusion.NewEngine) was only ever
constructed in tests (bf-2fz8), so pm.trackedBlobs was always nil — every
GetTrackedBlobs() reader (live loop, /api/blobs, anomaly adapter, API
wrappers) saw zero blobs and IO-6 could never pass.

Wire the engine into the live 10 Hz loop:
- Construct fusion.NewEngine at startup with a Config mirroring room geometry
  (Width/Height/Depth/Origin from the fleet room config), as a function-local
  retained for the life of main (mirrors selfImprovingLocalizer) so startup
  seeding and the follow-up node-seeding bead can call SetNodePosition on it
  and the live-loop closure captures it for Fuse each tick.
- Seed its node registry from the fleet at startup (without positioned nodes,
  Fuse skips every link).
- In the tracker goroutine, replace the dead pm.GetTrackedBlobs() read with
  fusionEngine.Fuse(gatherFusionLinks(pm)) -> blobTracker.track(result) ->
  pm.SetTrackedBlobs(blobs). The new blobTracker bridges the engine's
  stateless peaks to stateful sigproc.TrackedBlob IDs via greedy 1.0 m
  nearest-neighbour association (plan.md Component 4) and derives velocity
  from the previous position of the same tracked ID.

go build ./... / go vet ./... / go test ./... all pass.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 16:33:26 -04:00
jedarden
a7ec9d20b2 docs(signal): record bf-2fz8 finding — no engine feeds the live blob loop
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
Investigation (prereq for the bf-3gw1 umbrella) determining which fusion
engine produces the blobs consumed by the live 10 Hz loop and the IO-6 gate.

Verified findings (recorded as a doc comment on SetTrackedBlobs):
- pm.trackedBlobs is always nil: SetTrackedBlobs (signal/processor.go:625)
  has zero callers, so it is never invoked.
- Every GetTrackedBlobs reader therefore sees zero blobs — IO-6 ("walker
  produces a tracked blob") cannot pass. Read sites: live loop
  main.go:1866, /api/blobs main.go:4056, anomalyPositionAdapter main.go:4840,
  API wrappers internal/api/status.go:84 and internal/api/tracks.go:99.
- internal/fusion.NewEngine (3D Fresnel, internal/fusion/fusion.go:93) is
  NEVER constructed in non-test code; the internal/fusion package is imported
  by exactly one file, internal/localizer/fusion/timing_budget_test.go (a test).
- A second engine, internal/localization.NewEngine (2D Fresnel,
  internal/localization/fusion.go:53), IS constructed live via
  NewSelfImprovingLocalizer (main.go:1005) and runs Fuse (main.go:2593), but
  the returned *FusionResult is discarded — nothing reads .Peaks and no
  signal.TrackedBlob literal exists outside tests.

Conclusion: to satisfy IO-6, internal/fusion.NewEngine MUST be newly
constructed in non-test code and wired (Fuse -> TrackedBlob ->
SetTrackedBlobs), OR the existing localization.Engine FusionResult.Peaks must
be converted to TrackedBlobs and stored via SetTrackedBlobs.

Also adds fusion.Engine NodeCount()/NodePositions() accessors as scaffolding
for the umbrella's node-seeding verification.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 16:09:41 -04:00
jedarden
d299306cb0 test: drop stale PASS/FAIL reference in host-harness loop comment
The per-test setjmp loop's doc comment still said it lands back in the
loop "to print PASS/FAIL and advance" — but child 1 (bf-52k2) and
child 2 (bf-344n) already replaced both branches' outcome labels with
the neutral RUN marker family ("RUN: <name>" / "RUN: <name>
(assertion failed)"). The comment now contradicts the code, so align
it with the "neutral marker line" phrasing already used lower in the
same block. No code change; gcc -std=c11 -Wall -Wextra still clean.

Co-Authored-By: Claude <noreply@anthropic.com>
Bead-Id: bf-344n
2026-07-03 14:55:47 -04:00
jedarden
ca1b51007d test: drop stale PASS/FAIL reference in host-harness loop comment
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
The per-test setjmp loop's doc comment still said it lands back in the
loop "to print PASS/FAIL and advance" — but child 1 (bf-52k2) and
child 2 (bf-344n) already replaced both branches' outcome labels with
the neutral RUN marker family ("RUN: <name>" / "RUN: <name>
(assertion failed)"). The comment now contradicts the code, so align
it with the "neutral marker line" phrasing already used lower in the
same block. No code change; gcc -std=c11 -Wall -Wextra still clean.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 14:52:29 -04:00
jedarden
b86cb670a8 test: print neutral RUN marker on direct setjmp==0 path
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
The direct branch of main()'s per-test setjmp loop — taken when a test
runs normally without longjmping back — now prints "RUN: <name>" instead
of "PASS: <name>". This is the same neutral marker family child 1
(bf-52k2) chose for the else branch ("RUN: <name> (assertion failed)"),
so a passing test prints "RUN: <name>" and a failing test prints
"RUN: <name> (assertion failed)".

After this child, NEITHER branch emits PASS/FAIL; each test emits exactly
one neutral line. The internal passed++ counter is unchanged — it feeds
the run summary ("N passed, M failed of T"), not the per-test output, so
it is not an "observable label". g_failure_count accounting is likewise
untouched.

Child 2 of 4 for bf-38e9 (split-child 2 of bf-53ut).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 14:01:29 -04:00
jedarden
d9d1048320 Merge origin/main into local test-runner work
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
Reconciles divergence: local 8fbc2e8 (volatile doc) and origin's
4b0eaba (same) plus 5e58859 (nvs/csi/serial_prov host tests + CI
wiring) and fbb86fb (gitignore). Clean merge: both branches agree on
test_runner.c content; origin additionally adds host tests and CI.
2026-07-03 13:37:38 -04:00
jedarden
6e454803fb test: print neutral RUN marker on longjmp failure-return branch
The else branch of main()'s per-test setjmp loop — taken when a failed
assertion longjmps back — now prints "RUN: <name> (assertion failed)"
instead of "FAIL: <name>". The marker still names the failing test and
its own per-test `failed` counter, but contains no PASS/FAIL outcome
token (outcome labels are deferred to a later sibling of bf-53ut).

g_failure_count accounting is unchanged: test_record_failure() already
bumped it before the longjmp, so this branch only prints.

Child 1 of 4 for bf-38e9 (split-child 2 of bf-53ut).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 13:37:19 -04:00
jedarden
fbb86fb687 chore(firmware): gitignore abandoned idf.py --target linux test_apps residue
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
The ESP-IDF host-test path (idf.py test --target linux) was explicitly rejected
(decision record: docs/notes/firmware-host-test-approach.md) because firmware/main
builds as one idf_component_register whose REQUIRES names esp_wifi/bt/driver —
components with no host build, so the whole component is unhostable. Firmware host
tests instead run as a plain gcc harness under firmware/test/ (wired into CI via
the Dockerfile's 'RUN make -C test test').

firmware/test_apps/host_tests/ was leftover CMake/ninja build output from an
abandoned idf.py linux experiment (no source files). Gitignore firmware/test_apps/
so the rejected approach's artifacts can't be committed by accident.

Refs bead bf-31bp (host-test intent satisfied by the committed gcc harness).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 13:28:21 -04:00
jedarden
5e588592f4 test: add firmware host tests for nvs/csi/serial_prov + wire gcc harness into CI
Adds the three firmware host-test modules required by the Testing Strategy as a
plain gcc harness under firmware/test/ — NOT idf.py --target linux. That path was
rejected (docs/notes/firmware-host-test-approach.md, bf-21t): firmware/main
cannot host-link because csi.c pulls in esp_wifi.h and provision.c pulls in
driver/uart.h, and the single `main` component REQUIRES esp_wifi/bt/driver,
which have no linux build — so even nvs_migration.c (hostable in isolation) is
unhostable as part of the component. The harness therefore tests dependency-free
logic extractions and binary-format/wire contracts instead of linking the
firmware source.

- test_nvs_migration.c: fresh-install init to v1, no-downgrade guard, forward
  migration loop dispatch (v→v+1 at index v−1), and the concrete v1→v2 step
  (rename ms_ip→mothership_ip, default ntp_server), driven against an in-memory
  NVS store. Mirrors nvs_migration.c decision-for-decision.
- test_csi_frame.c: 24-byte header field round-trip, explicit little-endian
  timestamp byte order, signed-RSSI (uint8_t) reinterpretation, I/Q payload
  copy, n_sub=0 header-only probe, and the ingestion-side validation rules
  (too-short / payload-mismatch / n_sub>128 / bad channel). Mirrors the
  websocket.c encoder contract (offset/byte for offset/byte).
- test_serial_prov.c: provisioning JSON parser + NVS-mapping mirror of
  provision.c (all four protocol branches + every field mapping), shipping a
  bounded recursive-descent JSON decoder as the fuzz target. The fuzz pass
  (4000 random byte streams, a tricky-input corpus, 500 deep-nesting cases)
  proves the parser never crashes and the protocol always answers a single
  well-formed {"ok":...} line on any UART input.
- Makefile: gcc build/run recipe that globs every test_*.c + test_runner.c.

CI wiring: the Dockerfile firmware-builder stage now runs `make -C test test`
before the expensive ESP-IDF build, so a logic/format-contract regression fails
the image build fast. .gitignore + .dockerignore exclude the regenerable
host_tests binary.

docs/plan/plan.md Testing Strategy updated from the idf.py description to the
gcc harness (matching the decision record).

28 tests, all passing. go test ./... and go vet ./... unchanged (firmware-only).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 13:21:26 -04:00
jedarden
8fbc2e82c3 test: document setjmp volatile-analysis in firmware host harness loop
Add a comment next to the per-test setjmp(g_test_jmp) guard in main()'s
loop recording the C11 7.13.2.1 volatile analysis: the loop index i is
read in the post-longjmp path (g_tests[i].name) but is not modified
between the setjmp() call and a possible longjmp() — the body only reads
i, and the only write is the for-loop increment, which runs after control
returns. So no volatile qualifier is needed. (bf-2ftl)

Co-Authored-By: Claude <noreply@anthropic.com>
Bead-Id: bf-2ftl
2026-07-03 12:44:25 -04:00
jedarden
4b0eaba9a7 test: document setjmp volatile-analysis in firmware host harness loop
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
Add a comment next to the per-test setjmp(g_test_jmp) guard in main()'s
loop recording the C11 7.13.2.1 volatile analysis: the loop index i is
read in the post-longjmp path (g_tests[i].name) but is not modified
between the setjmp() call and a possible longjmp() — the body only reads
i, and the only write is the for-loop increment, which runs after control
returns. So no volatile qualifier is needed. (bf-2ftl)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 12:42:09 -04:00
jedarden
c66f62e616 test: land firmware host-test build scaffolding (Makefile, .gitignore, sanity)
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
The per-test setjmp/longjmp recovery loop in test_runner.c (main()) was
already delivered by sibling bead bf-bq9 (commit 549dc1f) and verified to
satisfy bf-53ut's contract: a failing assertion longjmps back to main()'s
setjmp, which falls through to advance the loop, so a failure in test N
never blocks tests N+1..end (proven with a temp failing-test followed by a
passing test — both ran and the process completed without aborting).

This commit lands the three harness files that were staged but absent from
HEAD (Makefile, .gitignore, test_sanity.c) so that 'make -C firmware/test
test' — the command bf-53ut's acceptance invokes — actually builds and runs
against the committed tree. Without them the committed test_runner.c could
not be built from a fresh clone.

Verified against the bf-53ut acceptance criteria:
- gcc -std=c11 -Wall -Wextra (-Werror): clean; no -Wclobbered/setjmp warnings
- normal suite: PASS, exit 0
- failing test + later passing test: both per-test lines print, process
  completes (non-fatal assertions)
- no changes to firmware/main/* or firmware/CMakeLists.txt

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 11:56:34 -04:00
jedarden
549dc1f179 test: add firmware host harness main driver — name-sort, setjmp loop, summary, exit code
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
Completes the runner (bf-bq9, child of bf-2i4). main() qsort()s the
registered tests by name for deterministic order regardless of constructor
or link order, drives each through the per-test setjmp/longjmp recovery
loop (print PASS on normal return, FAIL on longjmp return so one test's
failure never blocks the rest), prints a passed/failed/total summary, and
returns 1 iff g_failure_count > 0 — the non-zero-on-failure exit code CI
relies on via `make -C firmware/test test`.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 10:12:46 -04:00
jedarden
83250700ef test: add firmware host harness failure recovery — jmp_buf + test_record_failure longjmp
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
bf-3id (child of bf-2i4). Adds the per-test failure-recovery machinery to
firmware/test/test_runner.c:

- static jmp_buf g_test_jmp at file scope — the live setjmp() target the
  header ASSERT_* macros longjmp into; declared at file scope so the main()
  landing in bf-bq9 can setjmp() it directly before each test.
- static int g_failure_count — a run-wide counter test_record_failure()
  bumps on each failure, so main() can return non-zero on any failure.
- test_record_failure(file, line, fmt, ...) — prints file:line plus the
  vfprintf-formatted detail to stderr, bumps the counter, and longjmp()s
  into g_test_jmp so the current test aborts but the runner continues.

main() (the setjmp caller + PASS/FAIL reporting + non-zero exit) is
intentionally absent — it lands in the sibling bead bf-bq9. Until then this
TU compiles to an object (gcc -std=c11 -Wall -Wextra -c) but has no live
setjmp target for the longjmp, and does not link into a runnable harness.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 09:51:43 -04:00
jedarden
ef1360ee76 test: add firmware host harness test_register — append + capacity guard
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
Implement test_register() in firmware/test/test_runner.c, child 3 of the
bf-lfz sub-split. Appends each test entry at index g_test_count in GCC
constructor order and bumps the count, so the registry is fully populated
before main() runs. On a full registry (g_test_count >= MAX_TESTS) it logs
to stderr naming the skipped test and the cap and returns without writing
past the end — never overflows the array.

The static registry storage (g_tests/g_test_count) is now referenced, so the
__attribute__((unused)) it carried through child 2 (bf-uvv) is dropped.
test_record_failure() and main() remain intentionally absent (siblings
bf-3id and bf-bq9). Compiles cleanly to an object with zero warnings:
gcc -std=c11 -Wall -Wextra -Werror -c (no link, no main).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 09:13:52 -04:00
jedarden
18bce75f9e test: add firmware host harness registry storage — MAX_TESTS array + count
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
Child 2 of the bf-lfz sub-split (registry storage). Adds to
firmware/test/test_runner.c: #define MAX_TESTS 128, the static
test_entry_t g_tests[] array, and static g_test_count, with comments on why a
fixed static array is appropriate (GCC constructors from TEST() populate it
before main(); keeps the harness dependency-free; 128 is far more than the
handful of pure-logic host tests this harness targets).

The two statics are marked __attribute__((unused)): nothing reads or writes
the registry until child 3's test_register() lands, and gcc 14 (unlike the
older gcc the original "static file-scope symbols do not warn when unused"
assumption rested on) DOES warn on unused file-scope statics under
-Wall -Wextra. Without the attribute the object emits two -Wunused-variable
warnings and fails the bead's zero-warning build gate.

Compiles cleanly to an object with zero warnings:
  gcc -std=c11 -Wall -Wextra -c firmware/test/test_runner.c
No changes to firmware/main or firmware/CMakeLists.txt.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 09:04:13 -04:00
jedarden
191adbe9ad test: add firmware host harness runner skeleton — comment block + libc includes
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
Child 1 of 3 of the bf-lfz sub-split (bf-lfz is itself a child of bf-2i4;
grandparent bf-56v; great-grandparent bf-4ne; header API in bf-1xs). bf-lfz
failed three times as a single big-bang change, so its registry work is
decomposed into three atomic, independently-compilable pieces. This is the
skeleton (child 1):

- A top-of-file comment block documenting the incremental build-out: child 2
  adds the registry storage (the array + count), child 3 adds test_register(),
  and the failure handler + main() arrive in the sibling beads bf-3id and
  bf-bq9.
- Exactly the five includes the full runner will need: "test_runner.h" then
  <setjmp.h>, <stdio.h>, <stdlib.h>, <string.h> — libc only, no includes from
  firmware/main, by design (see test_runner.h's header comment and the bf-21t
  decision record).

With only the skeleton present this TU compiles cleanly to an object
(gcc -std=c11 -Wall -Wextra -c, zero warnings) but is not yet linkable into a
runnable harness: test_register() and test_record_failure() are still undefined
and there is no main(). Those arrive with the later children and siblings. No
registry array, count, or test_register() yet.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 08:37:30 -04:00
jedarden
128024c415 test: add firmware host harness header — TEST() macro + assert macros
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
Child 1 of the bf-56v harness split (grandparent bf-4ne). Establishes
the public header API (firmware/test/test_runner.h) that every test unit
and the runner compile against:

- TEST(name): declares a void body plus a GCC constructor
  (__attribute__((constructor))) that self-registers it via
  test_register(name, fn) before main() runs — so a new test_*.c is
  picked up with zero SOURCES edits.
- ASSERT_EQ / ASSERT_TRUE / ASSERT_FALSE: on mismatch, call
  test_record_failure(file, line, fmt...) which longjmp()s back to the
  per-test setjmp (the live jmp target lives in test_runner.c, a later
  child) — aborting only the current test, not the whole suite.
- Prototypes for test_register(const char*, test_fn) and
  test_record_failure(...).

Header comment records the bf-21t decision (gcc host harness, not
ESP-IDF --target linux: csi.c/provision.c are blocked by esp_wifi.h /
driver/uart.h, which have no linux build, and main is one component)
and names the single run command ('make -C firmware/test test') that a
later child makes real.

Self-contained: only libc (setjmp.h, stdbool.h, stdint.h); no
firmware/main/* includes; no main(). A trivial includer using TEST(x){}
compiles cleanly with 'gcc -std=c11 -Wall -Wextra -c'.

test_runner.c, the Makefile, and the sanity test belong to sibling
beads and are intentionally not part of this commit.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 06:57:58 -04:00
jedarden
9a3305bd23 docs: decide firmware host test approach — gcc harness over ESP-IDF linux (bf-21t)
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
Decision spike for the firmware/test/ host harness (split of bf-4ne).
Records why the ESP-IDF --target linux / Unity-host path was rejected:
csi.c is blocked by esp_wifi.h and provision.c by driver/uart.h, and
firmware/main builds as one component whose REQUIRES line names
esp_wifi/bt/driver — none of which have a linux build. Falls back to a
plain gcc harness under firmware/test/ that does not link main/*.c.
Reason must be carried into bf-4ne's test-runner header comment.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 06:01:55 -04:00
jedarden
598360e546 docs: refresh stale sections of plan.md to match implemented reality
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
Update docs/plan/plan.md to reflect the actual repo (VERSION 0.1.357):
- Go Module Layout: go.work stitches 3 modules (mothership, cmd/sim,
  test/acceptance); no test/integration/ — tests live in test/acceptance/,
  mothership/test/acceptance/, and tests/e2e/run.sh
- Dockerfile: espressif/idf:v5.2 firmware stage + golang:1.25-bookworm,
  GOOS/GOARCH pinned to linux/amd64 (single-arch, deliberate), image
  published as ronaldraygun/spaxel via the spaxel-build WorkflowTemplate
- Quality Gates item 4: document the amd64-only build decision (arm64 is
  future work) instead of the contradictory multi-arch gate
- Integration Tests: correct location to the real test dirs
- Open Questions: remove the duplicated Multi-installation coordination bullet
- Bump Last updated to 2026-07-03; Status → maintenance mode

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 00:54:16 -04:00
jedarden
c53bd96cc8 docs: add root README.md (repo standard, GitHub mirror landing page)
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
Covers Spaxel overview, realistic CSI capabilities, privacy framing, the
go.work multi-module repo layout, docker-compose quickstart with key env
vars, and pointers to docs/plan, docs/notes, docs/research, and the
dashboard README. Accurate to the current tree (image ronaldraygun/spaxel);
no secrets or host paths.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 00:42:11 -04:00
jedarden
83ec5a5ff7 docs: mark Phase 2 CSI recording buffer + adaptive sensing rate Done
Both are implemented (mothership/internal/recording/buffer.go and
mothership/internal/ingestion/ratecontrol.go). Phase 2 is now complete.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 00:17:13 -04:00
jedarden
9fabeb8a40 docs: mark Dashboard presence indicator Done in PROGRESS.md
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
Phase 2 table showed "Dashboard presence indicator" as Pending, but it
is implemented in dashboard/live.html and dashboard/js/app.js
(updatePresenceIndicator, #presence-indicator styling). Update the
table row to Done and drop it from the "Remaining for Phase 2" list.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 00:03:09 -04:00
jedarden
833b23d0a0 beads: plan-gap review 2026-07-02
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
2026-07-02 07:31:08 -04:00
jedarden
92bdf8caf5 merge: resolve VERSION conflict (0.1.356 + provision fix → 0.1.357)
Some checks failed
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Has been cancelled
2026-06-05 18:23:42 -04:00
jedarden
2483d36642 fix(provision): re-broadcast SPAXEL READY every 1s during window
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
Previously the ready signal was sent exactly once at window open, so the
browser had to have the serial port open at that exact millisecond after
reboot. Now the firmware broadcasts SPAXEL READY every second for the
full provisioning window (2 min fresh / 15 s reprov), giving the host
ample time to open the port and catch the handshake.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 18:23:25 -04:00
jedarden
9606210e19 ci: auto-bump version to 0.1.356 2026-06-05 22:15:41 +00:00
jedarden
cc65ae690f fix(acceptance): start mothership binary in SPAXEL_NO_DOCKER=1 path
Some checks failed
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Has been cancelled
The no-docker path created the cmd but never called cmd.Start(), so
waitForMothership always timed out. Add Start(), stdout/stderr wiring,
SPAXEL_BIND_ADDR, and SPAXEL_MDNS_ENABLED=false for CI headless operation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 01:34:07 -04:00
jedarden
f8ebf8f51f ci: auto-bump version to 0.1.355
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
2026-06-04 03:35:15 +00:00
jedarden
1175a330ed fix(lint): resolve golangci-lint v2 failures blocking CI
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
- .golangci.yml: add sort-results:true required by sort-order option
- ble/identity.go: replace deprecated reflect.Ptr with reflect.Pointer
- simulator/accuracy.go: remove ineffectual initial assignment to heightDesc
- .golangci.yml: add exclusions for cmd/sim/scenario.go (reconnect helper),
  test/acceptance/ (json.Decoder.Decode, unused type), and
  internal/fleet/fleethandler.go (unregistered handler methods)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 23:34:50 -04:00
jedarden
301a5884ce fix(firmware): bust Kaniko cache + force sdkconfig regen to fix 16MB crash loop
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
The 0.1.352 Docker image contained firmware compiled with CONFIG_ESPTOOLPY_FLASHSIZE=16MB
despite sdkconfig.defaults being updated to 4MB in d837598. Kaniko served a cached
firmware layer, bypassing the sdkconfig.defaults change.

Result: ESP32-S3 (4MB flash) flashed via Web Serial crashed on every boot:
  spi_flash: Detected size(4096k) smaller than binary image header(16384k). Probe failed.

Fix:
- Add FIRMWARE_CACHE_BUST ARG before COPY in firmware stage (guarantees cache miss)
- Add RUN rm -f sdkconfig sdkconfig.old so idf.py set-target regenerates from
  sdkconfig.defaults (CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y) on every build

Bumps version to 0.1.354 to trigger a fresh CI build.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 23:14:10 -04:00
jedarden
9ac200b719 fix(dashboard): serve embedded pages via io.ReadSeeker (fixes 500 on /fleet)
Some checks failed
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Has been cancelled
serveEmbeddedFile asserted the embedded file implemented
`interface { Len() int64; io.ReadSeeker }`, but *embed.openFile has no
Len() method, so http.ServeContent panicked (caught by chi Recoverer ->
500) on every embedded page: /fleet, /ambient, /live, /setup, /simple, /.
http.ServeContent only needs an io.ReadSeeker, which embed files satisfy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 20:43:30 -04:00
jedarden
9277217865 chore: bump version to 0.1.352 to build deployable image
Explicit VERSION bump so resolve-version skips its auto-bump git push
(which was racing with the e2e workflow), letting docker-build run with
the GOOS/GOARCH fix and push 0.1.352.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:42:13 -04:00
Argo Workflows CI
b687eafcbe ci: auto-bump version to 0.1.351 2026-05-26 17:38:48 +00:00
jedarden
1900128980 ci: trigger build with GOOS/GOARCH fix to produce a deployable image
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:38:26 -04:00
Argo Workflows CI
8150cb6cf4 ci: auto-bump version to 0.1.350 2026-05-26 17:27:10 +00:00
jedarden
4b8e6f793f fix(docker): pin GOOS/GOARCH to linux/amd64 in go build
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
The multi-arch change (2cd4410) derived GOOS/GOARCH from TARGETPLATFORM
with wrong cut field indices (-f2/-f3), yielding the invalid pair
amd64/amd64 -> `go: unsupported GOOS/GOARCH pair amd64/amd64`, failing
every CI image build since May 24. CI builds amd64 only (ESP-IDF firmware
is x86_64-only), so pin linux/amd64 explicitly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:26:26 -04:00
Argo Workflows CI
fe311a6b5f ci: auto-bump version to 0.1.349 2026-05-26 13:45:00 +00:00
jedarden
79f83369d3 ci: trigger build #2 to capture kaniko docker-build exit-2 logs
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 09:42:59 -04:00
jedarden
d5b7635133 ci: trigger diagnostic build to capture spaxel-build failure
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
Empty commit to fire the github webhook -> spaxel-build, so the failing
build step can be observed before podGC deletes the pod.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 09:31:21 -04:00
jedarden
bcd58a71d4 chore: bump version to 0.1.348 to build image carrying /spaxel-sim
Some checks failed
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Has been cancelled
Pins a deterministic tag for the image that includes the simulator binary
(added in 3ca6e8f), avoiding ambiguity with any in-flight 0.1.347 build
that predates the Dockerfile change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 21:54:11 -04:00