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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Builds cmd/sim alongside the mothership and copies /spaxel-sim into the
final image so the same image can drive a synthetic-node CSI load against
a deployed mothership. Default ENTRYPOINT still runs the mothership.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fix timezone mismatch between local time formatting and SQLite's UTC-based
date() function. AggregateDaily, GetWeeklyTrend, and GetAllWeeklyTrends now
use .UTC() before formatting dates to match SQLite's date(timestamp, 'unixepoch').
Closes: bf-26eg
Tests: TestHealthStore_DailyAggregation, TestHealthStore_GetAllWeeklyTrends now pass
- Fix SQL syntax errors caused by //nolint:errcheck comments inside raw string literals in anomaly.go
- Fix TestFlowAccumulator_DwellWhilePaused by establishing last waypoint before pausing
- Fix predictor_test.go compilation errors:
- Remove unused \"os\" import
- Replace AddTransitionSample with RecordTransition using ZoneTransition struct
- Fix map literal with missing key name
All tests now pass.
Closes: spaxel-test-fixes
- Convert pretty-printed JSON to proper line-delimited JSONL
- Fix bf-awtza bead: set closed_at for closed status
- Enables br sync --import-only to rebuild database
Implement IO-1 (Fresh install / first boot) and IO-2 (Idempotent restart)
acceptance tests for the hardware-free install and onboarding journey.
IO-1 validates:
- Fresh install starts with empty data volume
- First-run setup is accessible before PIN configuration
- PIN setup completes successfully
- Migrations run (detected in logs)
- PIN persists after setup
- Health check returns green
- No nodes are attached on fresh install
IO-2 validates:
- Configured install (PIN, node, zone) persists across restart
- Same data directory is reused after restart
- No re-setup prompt appears after restart
- Node label and position persist correctly
- Zone configuration persists correctly
- Mothership remains healthy after restart
These tests complete the IO-1..IO-11 acceptance test suite as specified
in docs/plan/plan.md, enabling hardware-free CI validation of the
installation and onboarding journey.
Closes: bf-2hi0h
Implements acceptance tests for failure scenarios and edge cases during
node onboarding per the plan specification:
- IO-7: Provisioning timeout - node that goes silent is marked offline
within heartbeat window (60s) and surfaced in /api/fleet; no crash
- IO-8: Bad/expired token - invalid token rejected with clear error;
node never enters fleet; no zombie row
- IO-9: Duplicate MAC - second connection with same MAC handled
(disconnects first or rejects second); no duplicate rows
- IO-10: Drop mid-onboard - killing simulator during onboarding leaves
node re-onboardable; no half-provisioned lock
- IO-11: Firmware-version skew - old firmware nodes onboard successfully
and OTA can be initiated
Tests use the acceptance harness with spaxel-sim and verify proper
handling of each scenario without mothership crashes or data corruption.
Closes: bf-1922s
Implements IO-6: Full new-user E2E (happy path) — HARD GATE.
The test verifies the complete onboarding journey from fresh install
to live events:
1. Fresh install + PIN setup
2. 6-node fleet onboarding via spaxel-sim
3. Define 2 zones + 1 portal
4. Run walker simulation
5. Verify blob detection, zone-presence events, portal-crossing
events, timeline entries, and MQTT/HA integration status
Added helper methods to TestHarness:
- CreateZone, CreatePortal, GetPortalCrossings
- GetTimeline, GetMQTTStatus
Closes: bf-1rifr
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implement acceptance tests for single-node and multi-node fleet onboarding:
- IO-3 (TestIO3_SingleNodeOnboarding): Validates end-to-end onboarding of a
single simulated ESP32 node via spaxel-sim. Verifies node transitions from
discovered to online, appears in /api/nodes within 10s, and that label/position
assignments persist via REST API (PUT /api/nodes/{mac}/position and PATCH
/api/nodes/{mac}/label).
- IO-4 (TestIO4_MultiNodeFleetBringup): Validates multi-node fleet bring-up with
6 nodes. Verifies all nodes reach online status, no TX-slot collision warnings
in logs, /api/nodes shows all 6 online, and fleet telemetry data is available
via /api/fleet endpoint.
Also fixes a context leak in TestIO5_DeviceIdentityBLEOnboarding by ensuring
the simulator context cancel function is called with defer.
Closes: bf-4jcjg
Quality gate #7: Verify nodes without valid tokens are rejected with HTTP 401.
- Created as7_auth_reject_test.go following AS1-AS6 pattern
- Tests WebSocket connection without X-Spaxel-Token header
- Verifies HTTP 401 Unauthorized response
- Validates simulator exits non-zero with invalid token
- Confirms no zombie nodes in fleet after rejection
- Registered AS7_AuthRejectIntegration in test runner
Closes: bf-2d9fj
Add migration_018 that creates all 8 prediction subsystem tables in the main
database, consolidating the separate prediction.db and prediction_accuracy.db
files. Add NewModelStoreWithDB and NewAccuracyTrackerWithDB constructors that
accept an existing *sql.DB connection, and update main.go to use the main
database connection instead of separate files.
- Added migration_018_add_prediction_tables with all prediction tables
- Added NewModelStoreWithDB() to accept shared DB connection
- Added NewAccuracyTrackerWithDB() to accept shared DB connection
- Updated Close() to only close DB when we own it (path != "")
- Updated main.go prediction init to use mainDB
The legacy NewModelStore() and NewAccuracyTracker() constructors remain for
backward compatibility (tests continue using their own migrations).
Closes: bf-38wcp
- Use chi.Walk instead of len/range on chi.Routes interface
- chi.Routes returns an interface type, not a slice, which cannot
be used with len() or range directly
- chi.Walk is the proper API for iterating registered routes
The test verifies that both Handler and FleetHandler can be registered
on the same router without chi panicking on duplicate routes.
Closes: bf-3o15x
The simulator API was registered at /simulator but the startup log
claimed /api/simulator. Fixed the route registration to match the
log and align with the REST API pattern (all endpoints under /api/).
The dashboard page route (/simulator serving simulator.html) remains
unchanged - only the API endpoint path was fixed.
Closes: bf-1f55j
- Add flag.Parse() call in TestMain to initialize testing flags before
running tests, fixing panic when test functions call testing.Short()
- Add proper PASS/FAIL reporting for each test in the sequence
- Apply go fmt formatting to io_install_upgrade_test.go
The IO-7..IO-11 failure and edge onboarding tests were already
implemented in the codebase. This fix ensures they can run properly
by initializing the testing framework before calling test functions.
Closes: bf-1922s
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implements IO-5 acceptance test which verifies:
- A person can be created via POST /api/people
- A simulated BLE device (from spaxel-sim) is discovered
- The BLE device can be assigned to a person via PUT /api/ble/devices/{mac}
- The device registration is persisted correctly with person_id, person_name, and person_color
Also fixes a bug in mothership/cmd/mothership/main.go where
SetBriefingProvider was called before dashboardHub was initialized,
causing a nil pointer dereference on startup. The call is now
made after the hub is created.
Closes: bf-3cagn (IO-5: BLE device-identity onboarding test)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implements plan §Disk Full Handling:
- Created internal/diskspace package with monitor goroutine (60s poll interval)
- At <100 MB free: stop CSI replay buffer writes, emit system alert event
- At <20 MB free: also pause crowd flow accumulation and prediction updates
- Detection and localization continue regardless of disk state
- Added /api/diskspace/stats endpoint for dashboard integration
Changes:
- internal/diskspace/monitor.go: Core monitor with state machine (normal/warning/critical)
- internal/diskspace/monitor_test.go: Unit tests for pause/resume behavior
- internal/recorder/manager.go: Added PauseWrites/ResumeWrites/IsPaused methods
- internal/recorder/manager_test.go: Tests for paused frame dropping
- internal/analytics/flow.go: Added PauseWrites/ResumeWrites/IsPaused methods
- internal/analytics/flow_test.go: Tests for paused trajectory/dwell accumulation
- internal/prediction/predictor.go: Added PauseUpdates/ResumeUpdates/IsPaused methods
- internal/prediction/predictor_test.go: Tests for paused prediction updates
- cmd/mothership/main.go: Integrated monitor initialization and API endpoint
All writes are no-ops while paused, with automatic resume when space recovers.
Closes: bf-4jb0a
Add support for 'predicted_enter' trigger condition that fires when a
prediction indicates a person is likely to enter a zone within a configured
time window (default 30 minutes). Uses rising-edge detection with 60-minute
cooldown per person-zone combination.
Changes:
- Add migration_017 to expand triggers table CHECK constraint to include
'predicted_enter' (SQLite table recreation required)
- Update volume store init() for new databases with expanded constraint
- Add predicted_enter to API validation in volume_triggers.go
- Implement evaluatePredictedEnter() in volume store with rising-edge
detection and cooldown tracking
- Add PredictionProvider interface and SetPredictionProvider() methods
to both volume.Store and automation.Engine
- Wire predicted_enter evaluation into 10 Hz fusion tick pipeline
Closes: bf-20sp3
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds three test cases for quality gate #7:
1. TestAS7_AuthRejectMissingToken - verifies nodes without tokens are rejected
2. TestAS7_AuthRejectInvalidToken - verifies nodes with invalid tokens are rejected
3. TestAS7_AuthAcceptValidToken - verifies nodes with valid tokens are accepted
Each test verifies:
- Simulator exits non-zero on rejection
- Mothership logs the rejection
- No nodes connect when auth fails
Closes: bf-2d9fj
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>