test(13-01): add failing tests for InterfaceInfo collector

- InterfaceInfo struct field compilation test
- MAC address lowercasing test
- Running bool parsing test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jason Staack
2026-03-19 06:01:12 -05:00
parent caa33ca8d7
commit 4b5bb949e9
4 changed files with 109 additions and 31 deletions

View File

@@ -29,12 +29,12 @@
### Wireless Collection
- [ ] **WRCL-01**: Poller collects per-client registration table data from APs (MAC, signal, CCQ, TX/RX rates, distance, uptime) on a 5-minute cadence
- [ ] **WRCL-02**: Poller collects per-interface RF stats (noise floor, channel width, TX power, registered client count) via monitor command
- [ ] **WRCL-03**: Per-client wireless data publishes to a dedicated NATS stream (separate from DEVICE_EVENTS) to prevent stream saturation
- [ ] **WRCL-04**: Per-client wireless data stores in a dedicated hypertable with 30-day retention (separate from existing wireless_metrics)
- [ ] **WRCL-05**: Poller handles RouterOS v6/v7 field differences gracefully (CCQ absent in v7 wifi package)
- [ ] **WRCL-06**: Signal strength parsing handles RouterOS format variations (e.g., `-67@5GHz` suffix)
- [x] **WRCL-01**: Poller collects per-client registration table data from APs (MAC, signal, CCQ, TX/RX rates, distance, uptime) on a 5-minute cadence
- [x] **WRCL-02**: Poller collects per-interface RF stats (noise floor, channel width, TX power, registered client count) via monitor command
- [x] **WRCL-03**: Per-client wireless data publishes to a dedicated NATS stream (separate from DEVICE_EVENTS) to prevent stream saturation
- [x] **WRCL-04**: Per-client wireless data stores in a dedicated hypertable with 30-day retention (separate from existing wireless_metrics)
- [x] **WRCL-05**: Poller handles RouterOS v6/v7 field differences gracefully (CCQ absent in v7 wifi package)
- [x] **WRCL-06**: Signal strength parsing handles RouterOS format variations (e.g., `-67@5GHz` suffix)
### Link Discovery
@@ -107,12 +107,12 @@
| SECT-01 | Phase 14 | Pending |
| SECT-02 | Phase 14 | Pending |
| SECT-03 | Phase 14 | Pending |
| WRCL-01 | Phase 12 | Pending |
| WRCL-02 | Phase 12 | Pending |
| WRCL-03 | Phase 12 | Pending |
| WRCL-04 | Phase 12 | Pending |
| WRCL-05 | Phase 12 | Pending |
| WRCL-06 | Phase 12 | Pending |
| WRCL-01 | Phase 12 | Complete |
| WRCL-02 | Phase 12 | Complete |
| WRCL-03 | Phase 12 | Complete |
| WRCL-04 | Phase 12 | Complete |
| WRCL-05 | Phase 12 | Complete |
| WRCL-06 | Phase 12 | Complete |
| LINK-01 | Phase 13 | Pending |
| LINK-02 | Phase 13 | Pending |
| LINK-03 | Phase 13 | Pending |

View File

@@ -35,7 +35,7 @@ v9.7 transforms TOD from a flat device list into a site-aware fleet management p
- Decimal phases (11.1, 11.2): Urgent insertions (marked with INSERTED)
- [x] **Phase 11: Site Data Model + Foundation** - Sites CRUD, device assignment, site list with health rollup (completed 2026-03-19)
- [ ] **Phase 12: Per-Client Wireless Collection** - Poller extension to collect registration table and per-interface RF stats
- [x] **Phase 12: Per-Client Wireless Collection** - Poller extension to collect registration table and per-interface RF stats (completed 2026-03-19)
- [ ] **Phase 13: Link Discovery + Registration Ingestion** - Backend NATS consumer, MAC resolution, AP-CPE link state machine
- [ ] **Phase 14: Site Dashboard + Sector Views + Wireless UI** - Site detail page, sector-centric view, per-station wireless tables
- [ ] **Phase 15: Signal Trending + Site Alerting** - Signal history charts, degradation detection, site/sector alert rules
@@ -69,7 +69,7 @@ Plans:
3. Per-client data publishes to a dedicated WIRELESS_REGISTRATIONS NATS stream (not DEVICE_EVENTS)
4. Per-client data stores in a dedicated hypertable with 30-day retention
5. Collection works correctly on both RouterOS v6 (wireless package) and v7 (wifi package) with graceful handling of missing fields
**Plans:** 2 plans
**Plans:** 2/2 plans complete
Plans:
- [ ] 12-01-PLAN.md — Go poller per-client registration collector, signal parser, RF monitor, NATS stream and publisher
@@ -84,11 +84,12 @@ Plans:
2. Link state follows a temporal state machine (discovered, active, degraded, down, stale) with consecutive-miss threshold to prevent false flapping
3. Discovered links are stored in a materialized wireless_links table for fast dashboard queries
4. Wireless clients whose MACs do not match any managed device appear as "unknown clients" with their signal and rate data preserved
**Plans**: TBD
**Plans:** 3 plans
Plans:
- [ ] 13-01: TBD
- [ ] 13-02: TBD
- [ ] 13-01-PLAN.md — Go poller interface collector (/interface/print) and DEVICE_EVENTS publisher
- [ ] 13-02-PLAN.md — Backend device_interfaces and wireless_links table migrations with ORM models
- [ ] 13-03-PLAN.md — Link discovery subscriber, interface subscriber, link REST API, and app wiring
### Phase 14: Site Dashboard + Sector Views + Wireless UI
**Goal**: Operators can drill into any site to see device health, sector-organized AP/CPE views, and per-station wireless details on device pages
@@ -129,8 +130,7 @@ Plans:
| Sites | SITE-01, SITE-02, SITE-03, SITE-04, SITE-05, SITE-06 | 11 | 3/3 | Complete | 2026-03-19 | DASH-01 | 11 | 1 |
| Site Dashboard | DASH-02, DASH-03, DASH-04 | 14 | 3 |
| Sectors | SECT-01, SECT-02, SECT-03 | 14 | 3 |
| Wireless Collection | WRCL-01, WRCL-02, WRCL-03, WRCL-04, WRCL-05, WRCL-06 | 12 | 6 |
| Link Discovery | LINK-01, LINK-02, LINK-03, LINK-04 | 13 | 4 |
| Wireless Collection | WRCL-01, WRCL-02, WRCL-03, WRCL-04, WRCL-05, WRCL-06 | 12 | 2/2 | Complete | 2026-03-19 | LINK-01, LINK-02, LINK-03, LINK-04 | 13 | 4 |
| Wireless UI | WRUI-01, WRUI-02, WRUI-03 | 14 | 3 |
| Signal Trending | TRND-01, TRND-02 | 15 | 2 |
| Site Alerting | ALRT-01, ALRT-02 | 15 | 2 |
@@ -145,7 +145,7 @@ Phases execute in numeric order: 11 -> 11.x -> 12 -> 12.x -> 13 -> 13.x -> 14 ->
|-------|----------------|--------|-----------|
| 11. Site Data Model + Foundation | 0/3 | Planning complete | - |
| 12. Per-Client Wireless Collection | 0/2 | Planning complete | - |
| 13. Link Discovery + Registration Ingestion | 0/? | Not started | - |
| 13. Link Discovery + Registration Ingestion | 0/3 | Planning complete | - |
| 14. Site Dashboard + Sector Views + Wireless UI | 0/? | Not started | - |
| 15. Signal Trending + Site Alerting | 0/? | Not started | - |

View File

@@ -2,14 +2,14 @@
gsd_state_version: 1.0
milestone: v9.7
milestone_name: Tower & Site Management
status: phase-complete
stopped_at: Completed 11-03-PLAN.md
last_updated: "2026-03-19T02:53:16Z"
status: unknown
stopped_at: Completed 12-01-PLAN.md
last_updated: "2026-03-19T10:40:03.896Z"
progress:
total_phases: 5
completed_phases: 1
total_plans: 3
completed_plans: 3
completed_phases: 2
total_plans: 5
completed_plans: 5
---
# Project State
@@ -19,12 +19,12 @@ progress:
See: .planning/PROJECT.md (updated 2026-03-18)
**Core value:** Operators can monitor, configure, and troubleshoot their entire MikroTik fleet from a single pane of glass
**Current focus:** Phase 11site-data-model-foundation
**Current focus:** Phase 12per-client-wireless-collection
## Current Position
Phase: 11 (site-data-model-foundation) — COMPLETE
Plan: 3 of 3 (all complete)
Phase: 12 (per-client-wireless-collection) — COMPLETE
Plan: 2 of 2 (all complete)
## Performance Metrics
@@ -45,6 +45,9 @@ Plan: 3 of 3 (all complete)
| Phase 11 P01 | 3min | 2 tasks | 9 files |
| Phase 11 P02 | 6min | 3 tasks | 8 files |
| Phase 11 P03 | 3min | 2 tasks | 5 files |
| Phase 12 P01 | 3min | 2 tasks | 6 files |
| Phase 12 P02 | 3min | 2 tasks | 3 files |
| Phase 12 P01 | 3min | 2 tasks | 6 files |
### Decisions
@@ -58,6 +61,9 @@ Decisions are logged in PROJECT.md Key Decisions table.
- [Phase 11]: Used Dialog for delete confirmation (no AlertDialog component in UI library)
- [Phase 11]: Site column placed after Model in fleet table for logical grouping
- [Phase 11]: Viewers see site name text, operators get Select dropdown for assignment
- [Phase 12]: Used unified tenant_isolation RLS policy with super_admin OR clause (matching codebase convention) instead of separate super_admin_bypass policy
- [Phase 12]: WIRELESS_REGISTRATIONS NATS stream uses 30-day retention (vs 24h for DEVICE_EVENTS) for historical client analytics
- [Phase 12]: RF monitor collection gated on wireless interface presence to avoid unnecessary API calls
### Pending Todos
@@ -71,6 +77,6 @@ None yet.
## Session Continuity
Last session: 2026-03-19T02:53:16Z
Stopped at: Completed 11-03-PLAN.md (Phase 11 complete)
Last session: 2026-03-19T10:40:03.893Z
Stopped at: Completed 12-01-PLAN.md
Resume file: None

View File

@@ -0,0 +1,72 @@
package device
import (
"testing"
)
func TestInterfaceInfoFields(t *testing.T) {
// Verify struct compiles with expected fields and JSON tags.
info := InterfaceInfo{
Name: "ether1",
MacAddress: "aa:bb:cc:dd:ee:ff",
Type: "ether",
Running: true,
}
if info.Name != "ether1" {
t.Errorf("Name = %q, want %q", info.Name, "ether1")
}
if info.MacAddress != "aa:bb:cc:dd:ee:ff" {
t.Errorf("MacAddress = %q, want %q", info.MacAddress, "aa:bb:cc:dd:ee:ff")
}
if info.Type != "ether" {
t.Errorf("Type = %q, want %q", info.Type, "ether")
}
if !info.Running {
t.Error("Running = false, want true")
}
}
func TestInterfaceMACLowercasing(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{name: "already lowercase", input: "aa:bb:cc:dd:ee:ff", want: "aa:bb:cc:dd:ee:ff"},
{name: "uppercase", input: "AA:BB:CC:DD:EE:FF", want: "aa:bb:cc:dd:ee:ff"},
{name: "mixed case", input: "Aa:Bb:Cc:Dd:Ee:Ff", want: "aa:bb:cc:dd:ee:ff"},
{name: "empty", input: "", want: ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := normalizeMACAddress(tt.input)
if got != tt.want {
t.Errorf("normalizeMACAddress(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
}
func TestInterfaceRunningParsing(t *testing.T) {
tests := []struct {
name string
input string
want bool
}{
{name: "true string", input: "true", want: true},
{name: "false string", input: "false", want: false},
{name: "empty string", input: "", want: false},
{name: "yes string", input: "yes", want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := parseRunning(tt.input)
if got != tt.want {
t.Errorf("parseRunning(%q) = %v, want %v", tt.input, got, tt.want)
}
})
}
}