ePMP devices were not reporting firmware_version because the YAML's
cambiumCurrentuImageVersion.0 OID returns empty on ePMP3000 firmware.
Add detect_version/1 to the Epmp vendor module with two-OID fallback:
cambiumCurrentSWInfo.0 (verbose, ePMP3000-friendly) → cambiumCurrentuImageVersion.0
(short, ePMP1000/2000), with a regex to extract the version triplet
from whatever the device returns.
ePMP3000 also has a sysObjectID quirk where some firmware reports the
generic NetSNMP-on-Linux ID (.1.3.6.1.4.1.8072.3.2.10) instead of the
Cambium enterprise OID, causing devices to misclassify as the linux
profile. Add a second discovery block to the epmp profile that matches
NetSNMP-OID + PREEMPT_RT armv7l kernel signature + a confirming snmpget
against the Cambium ePMP MIB tree, so detection runs in pass-2
(conditional non-generic) before linux falls through in pass-3.
- New `sync_worker_perform_test.exs` drives every cron sync worker's
`perform/1` body via `Req.Test` stubs, exercising the {:error, _}
branch of `sync_integration/1` for Sonar, Splynx, Visp, and NetBox,
plus the should_sync?-skip path.
- OnboardingLive: covers `finish` event, `skip` from :agent (both
trigger complete_onboarding), and `go_to_step :agent` (which calls
the previously-untested `load_agent_token/1`).
- GaiiaInsightWorker: adds a test that drives `generate_mismatch_insights/2`
by inserting an inventory item whose ip_address disagrees with its
mapped device.
Coverage: 86.52% → 86.61%.
Adds a dedicated test file that drives the small private mapping helpers
inside `Towerops.Topology` via their public surface:
- `role_to_type_atom/1`: every device_role string (router, core_router,
switch, distribution_switch, access_switch, access_point, cpe,
backhaul, backhaul_radio, server, firewall, ups, other, unknown
fallback) is exercised through `device_to_node` (via
`get_topology_for_map/2`) and again through `get_node_detail/2`.
- `link_type_priority/1`: each priority pair (lldp/cdp/mac_match/
arp_inference) is exercised via `merge_bidirectional_edges/1` by
upserting reciprocal links of differing types and asserting the
higher-priority `link_type` survives the merge.
- `evidence_type_to_link_type/1`: every evidence_type clause
(lldp_neighbor, cdp_neighbor, mac_on_interface, arp_entry,
wireless_registration, plus the catch-all) is hit via
`upsert_link/1` → `process_evidence_for_link/2`.
Coverage: 86.28% → 86.42% (+0.14%).
Adds focused tests across multiple modules to push toward the 87% threshold:
- CoreComponents: pagination/1 (multi-page, ellipsis, params),
list/1 (slot rendering), and signal_badge/1 across all RSSI bands.
- ApiDocs/GraphQLDocs controllers: bypass-the-router pattern so the
"scope-with-organization" branches are actually exercised (the public
routes are unscoped, so the org branch is unreachable via get/2).
- Mix.Tasks.Unused: text + JSON output, --only / --skip filters.
- Mix.Tasks.JobCleanupTask: prod path with phoenix snmp enabled vs
disabled (uses :job_cleanup_settle_ms = 0 to avoid the 1s sleep).
- Member resolver: success path for remove/update_role on non-owner
membership, and the changeset-error branch for an invalid role.
- Check resolver: unauthenticated fallback clauses for list / get /
create / update / delete.
- RfLinks: nil-signal sort ordering, :unknown classification for
nil/nil signal+snr, :degraded filter, capacity_utilization edge
cases (max_rate 0/missing, both rates nil).
- StatusHelpers: status_emoji + title_with_status across green / red
branches (warning is unreachable given the active-alerts query).
- ApiTokens: spawn_async_update/2 path with env != :test.
- ImpactAnalysis: to_json/1 across nil and populated levels.
- Maintenance: query helpers (default-arity wrappers, active range)
and Show LiveView delete event redirecting to /maintenance.
- Coverage map: probe_point event handler.
- InvitationQuery: default-arity pending/1.
- Remove Mox.set_mox_global() in DPW extra test; rely on $callers
instead so concurrent vendor SNMP tests stop intermittently failing
with VerificationError.
- Make ToweropsWeb.Plugs.GraphQLIntrospectionTest async: false. It
mutates Application.put_env(:towerops, :env, :prod), which polluted
every async vendor test that reads :env via
Towerops.Snmp.Client.phoenix_snmp_disabled/0.
- Bump test pool queue_target/queue_interval. AgentChannelTest spawns
many `:proc_lib` channel processes that hold sandbox connections;
the default 50ms queue caused intermittent
'could not checkout the connection' errors.
- Tag real-System.cmd("ping") tests as :network so they're excluded
by default. Saves ~20s on every full run.
- Make JobCleanupTask's internal 1s settle sleep configurable via
:job_cleanup_settle_ms; the test overrides it to 0.
Plus the pending CoverageLive.Form edit-save / EIRP recompute and
TraceLive deep-link tests from before the compact.
Add PromEx with Application/BEAM/Phoenix/LiveView/Ecto/Oban plugins plus
a custom plugin that bridges existing Towerops Oban/Redis telemetry events.
The metrics HTTP server runs isolated on port 9568 so scrape traffic never
traverses the public Traefik IngressRoute.
The pod template carries prometheus.io annotations (scrape, port, path, job)
so the external Prometheus on 10.0.15.31 discovers each pod through the
existing kubernetes-pods scrape job (apiserver-proxy).
- Add Codeberg Ansible collection link to API/GraphQL docs and Terraform guide
- Un-skip the previously-disabled DevicePollerWorker tests that still pass
- Expand MibController, CnMaestro.Sync, CheckWorker, ActivityFeed,
MobileController, JobCleanupTask tests
- New UploadMibs Mix task test covering arg validation and upload paths
- Set :mib_dir in test config so MibController upload/delete flows can
exercise the real on-disk paths against a writable tmp directory
The /docs/api and /docs/graphql pages were missing four resource
families that the API has shipped:
- Coverages (RF coverage prediction with async compute, recompute,
KMZ download)
- Maintenance Windows (one-off + recurring with RRULE)
- On-Call Schedules (top-level CRUD + layers, members, overrides,
on_call lookup)
- Escalation Policies (CRUD + rules + targets)
Both REST and GraphQL pages now have sidebar entries and example
requests for each.
The current ApiDocs/GraphQLDocs templates don't surface the sample_token
back into the rendered HTML, so the previous assertions were checking
for strings that don't exist. Drop them down to a 'page renders' check.
The controller branches that compute sample_token still get exercised.
Add focused tests across many modules to push overall coverage from
75.45% to ~77%.
New test files:
- test/towerops/snmp/topology_test.exs
- test/towerops/vault_test.exs
- test/towerops/workers/check_worker_test.exs
- test/towerops_web/graphql/resolvers/happy_path_test.exs
- test/towerops_web/graphql/schema_test.exs
- test/towerops_web/live/reports_live_test.exs
Expanded existing tests for: Airos vendor, ProfileWatcher, StormDetector,
LLDP, GpsSync, MikrotikBackupWorker, AdminController, MibController,
MobileController, GeoipController, OnboardingLive, UserResetPasswordLive,
SessionManager, Telemetry, CoverageLive.Show.
Also fix a pre-existing dead test in ApplicationSettingTest where the
schema default for value_type made the 'invalid without value_type' test
unreachable.
Apply Testing LiveView course patterns (live/2, has_element?,
render_click, form/3). Add tests for MobileQRLive, WeathermapLive,
StatusPageLive, controllers, plugs, and pure function modules.
Expand proto decode coverage to 85.24%.
Dialyzer:
* Add Lidar.Tile @type, narrow several @specs to match success-typing,
prefix discarded function returns with `_ =`, fix the broken
File.stream! call in the ms-buildings worker (it was causing dialyzer
to mark every private fun unreachable), broaden Antenna.spec key
types from `float()` to `number()` to match catalog int literals.
* Add :geo, :geo_postgis, :db_connection to plt_add_apps so the dep
PLT actually contains the modules we use.
* Result: 0 unsuppressed dialyzer warnings (was 50+ in project code).
Tree canopy:
* Replace the dead landfire.gov URL with the ETH Zurich Global
Canopy Height 10 m product. ETH publishes proper COGs with
overviews on libdrive.ethz.ch — confirmed working with
/vsicurl/ byte-range reads. The module now picks the 3°×3° tile
containing the bbox centroid, with both a `:metres` decoder
(ETH Byte raster, NoData=255) and a `:landfire_evh` decoder for
operators self-hosting LANDFIRE EVH COGs.
* Re-enable canopy by default now that there's a working source.
Tests (+105 tests, 0 failures):
* CoverageLive Index/Show/Form/Map — empty/loaded/auth states,
LiveView event handlers, formatters.
* Coverages.Building changeset + helpers.
* Coverages.TreeCanopy provider tile-URL builder and decoders.
* Workers.MsBuildingsImportWorker (full import flow incl. blank
lines, MultiPolygon, idempotency, hash fallback) — also fixed
the partial-index ON CONFLICT bug that prevented imports from
ever working: the unique index has WHERE ms_footprint_id IS
NOT NULL, so the worker now uses an :unsafe_fragment conflict
target that includes the predicate.
* GraphQLDocsController smoke test (was 0%).
Coverage rose from 73.19% → 74% with the major project modules I
touched all moved from 0% / <50% to >75%.