Files
MIKROTIK-RMM/scripts/seed-capsman-fixture.sql
2GT-RichandClaude Opus 5 5071e1f512 chore(dev): seedable CAPsMAN fixture so managed-wireless paths can be tested (v0.23.8-beta)
Two bugs shipped in the last three releases because CAPsMAN code was written and
"verified" against a fleet that contains no CAPsMAN. Neither was reachable by unit
tests, because both lived in SQL: a join that could never match, and a WHERE clause
that filtered out every managed radio before the analysis ran. Both were found by
the reporter, in production, after release.

The fix is not another test of the pure functions — those were already right. It is
to stop the local environment from being unrepresentative.

scripts/seed-capsman-fixture.sql builds a deployment shaped like a real one:

  - the controller is a ROUTER with no radios of its own, so anything keyed on
    device_type = 'wireless_ap' misses it
  - CAP interfaces carry NULL frequency, SSID and band, because the controller owns
    the configuration — the exact condition that removed them from Channel Usage
  - the controller mirrors every CAP interface locally, so a CAP's radio MAC
    legitimately appears on two devices — the condition behind the misattribution
  - channels exist only as "<freq>/<phy>/<control-positions>", never a plain number
  - the radios are arranged to produce one partial overlap and one co-channel pair,
    so interference classification has something real to get wrong

Running it against v0.23.7 confirms the fix shipped blind two commits ago actually
works: six managed radios now group under the controller, the controller appears in
the location selector, the standalone group holds only the genuinely standalone AP,
5680/ax/eCee/D resolves to 80 MHz, client counts come from the controller where the
CAP rows read zero, and the controller's mirror interfaces do not double-count.

Documented in docs/architecture.md, including the point that when a report arrives
with device output attached, that output is the most valuable thing in it and
belongs in the fixture.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 20:21:19 -07:00

110 lines
6.3 KiB
PL/PgSQL

-- A representative CAPsMAN deployment for local development.
--
-- Two bugs shipped in v0.23.5 and v0.23.6 because CAPsMAN code was written and
-- "verified" against a fleet that contains no CAPsMAN at all. Neither was catchable
-- by unit tests: both lived in SQL — a join that could not match, and a WHERE clause
-- that filtered out every managed radio before the analysis ran.
--
-- This reproduces the shape of a real deployment (reported on issue #94) so those
-- paths can be exercised locally against the actual endpoints:
--
-- * a controller that is a ROUTER with no radios of its own
-- * CAPs whose own wireless_interfaces rows carry NULL frequency and NULL ssid,
-- because the controller owns the configuration
-- * the controller's local mirror of each CAP interface, which makes a CAP's radio
-- MAC legitimately appear on two devices
-- * channels only in RouterOS's "<freq>/<phy>/<control-positions>" form
--
-- Everything is prefixed `fixture-` and removed by seed-capsman-fixture-clean.sql.
BEGIN;
-- Controller: a router. Deliberately NOT device_type 'wireless_ap', because that
-- misclassification is what hid controllers from the wireless section originally.
INSERT INTO devices (name, ip_address, api_username, api_password_encrypted,
device_type, status, model, wifi_role, wifi_package)
VALUES ('fixture-core-rt-001', '10.250.0.1', 'admin', 'x',
'router', 'online', 'RB5009UPr+S+', 'controller', 'wifi')
ON CONFLICT DO NOTHING;
INSERT INTO devices (name, ip_address, api_username, api_password_encrypted,
device_type, status, model, wifi_role, wifi_package)
VALUES ('fixture-hall-ap-001', '10.250.0.18', 'admin', 'x', 'wireless_ap', 'online', 'cAP ax', 'cap', 'wifi'),
('fixture-left-ap-001', '10.250.0.20', 'admin', 'x', 'wireless_ap', 'online', 'cAP ax', 'cap', 'wifi'),
('fixture-right-ap-001', '10.250.0.22', 'admin', 'x', 'wireless_ap', 'online', 'cAP ax', 'cap', 'wifi')
ON CONFLICT DO NOTHING;
-- CAP-side interfaces: no ssid, no frequency, no band. This is what a CAP actually
-- reports, and assuming otherwise is what removed them from Channel Usage.
INSERT INTO wireless_interfaces (device_id, name, ssid, frequency, channel_width, band,
disabled, running, registered_clients, managed_by_capsman,
radio_mac, mac_address, config_json)
SELECT d.id, v.name, NULL, NULL, NULL, NULL, FALSE, TRUE, 0, TRUE, v.mac, v.mac, '{}'::jsonb
FROM (VALUES
('fixture-hall-ap-001', 'wifi1', '04:F4:1C:A2:C4:59'),
('fixture-hall-ap-001', 'wifi2', '04:F4:1C:A2:C4:5A'),
('fixture-left-ap-001', 'wifi1', '04:F4:1C:A3:1E:7B'),
('fixture-left-ap-001', 'wifi2', '04:F4:1C:A3:1E:7C'),
('fixture-right-ap-001', 'wifi1', '04:F4:1C:A5:0C:0D'),
('fixture-right-ap-001', 'wifi2', '04:F4:1C:A5:0C:0E')
) AS v(dev, name, mac)
JOIN devices d ON d.name = v.dev
ON CONFLICT (device_id, name) DO NOTHING;
-- The controller mirrors every CAP interface locally, so each CAP radio MAC appears
-- on two devices. A single-valued MAC index picked whichever row the database
-- returned first, which is how radios ended up attributed to the controller.
INSERT INTO wireless_interfaces (device_id, name, ssid, frequency, channel_width,
disabled, running, registered_clients, managed_by_capsman,
radio_mac, mac_address, config_json)
SELECT d.id, v.name, NULL, NULL, NULL, FALSE, TRUE, 0, FALSE, v.mac, v.mac, '{}'::jsonb
FROM (VALUES
('wifi-fixture-hall-2g', '04:F4:1C:A2:C4:5A'),
('wifi-fixture-hall-5g', '04:F4:1C:A2:C4:59'),
('wifi-fixture-left-2g', '04:F4:1C:A3:1E:7C'),
('wifi-fixture-left-5g', '04:F4:1C:A3:1E:7B'),
('wifi-fixture-right-2g', '04:F4:1C:A5:0C:0E'),
('wifi-fixture-right-5g', '04:F4:1C:A5:0C:0D')
) AS v(name, mac)
CROSS JOIN devices d
WHERE d.name = 'fixture-core-rt-001'
ON CONFLICT (device_id, name) DO NOTHING;
-- Controller's view of the radios it manages. Channels only in RouterOS form, and
-- deliberately including a partial overlap (2412 vs 2422) plus a co-channel pair
-- (5680 twice) so the interference analysis has something real to classify.
INSERT INTO capsman_radios (controller_device_id, radio_mac, interface_name, local, hw_type,
current_channel, matched_device_id, state, registered_peers,
authorized_peers, tx_power, ssid, config_json)
SELECT c.id, v.mac, v.iface, FALSE, 'QCA6018', v.chan, a.id, 'running', v.peers, v.peers, 20, v.ssid, '{}'::jsonb
FROM (VALUES
('04:F4:1C:A2:C4:5A', 'wifi-fixture-hall-2g', '2412/ax/Ce', 'fixture-hall-ap-001', 8, 'Sunflower'),
('04:F4:1C:A2:C4:59', 'wifi-fixture-hall-5g', '5680/ax/eCee/D', 'fixture-hall-ap-001', 1, 'Sunflower'),
('04:F4:1C:A3:1E:7C', 'wifi-fixture-left-2g', '2422/ax/Ce', 'fixture-left-ap-001', 5, 'Sunflower'),
('04:F4:1C:A3:1E:7B', 'wifi-fixture-left-5g', '5680/ax/eCee/D', 'fixture-left-ap-001', 7, 'Sunflower'),
('04:F4:1C:A5:0C:0E', 'wifi-fixture-right-2g', '2462/ax/Ce', 'fixture-right-ap-001', 3, 'Sunflower'),
('04:F4:1C:A5:0C:0D', 'wifi-fixture-right-5g', '5500/ax/Ceeeeeee/D','fixture-right-ap-001', 20, 'Sunflower')
) AS v(mac, iface, chan, cap, peers, ssid)
JOIN devices c ON c.name = 'fixture-core-rt-001'
JOIN devices a ON a.name = v.cap
ON CONFLICT (controller_device_id, radio_mac) DO NOTHING;
INSERT INTO capsman_configurations (controller_device_id, name, ssid, mode, band, security, authentication_types, config_json)
SELECT c.id, v.name, v.ssid, 'ap', v.band, 'public_security', 'wpa2-psk,wpa3-psk', '{}'::jsonb
FROM (VALUES ('public_conf', 'Sunflower', NULL), ('public_conf_2g', 'Sunflower', '2ghz-ax'))
AS v(name, ssid, band)
JOIN devices c ON c.name = 'fixture-core-rt-001'
ON CONFLICT (controller_device_id, name) DO NOTHING;
INSERT INTO capsman_provisioning (controller_device_id, ros_id, action, master_configuration, disabled, config_json)
SELECT c.id, v.rid, 'create-dynamic-enabled', v.cfg, FALSE, '{}'::jsonb
FROM (VALUES ('*f1', 'public_conf'), ('*f2', 'public_conf_2g')) AS v(rid, cfg)
JOIN devices c ON c.name = 'fixture-core-rt-001'
ON CONFLICT (controller_device_id, ros_id) DO NOTHING;
COMMIT;
SELECT 'seeded' AS status,
(SELECT count(*) FROM devices WHERE name LIKE 'fixture-%') AS devices,
(SELECT count(*) FROM capsman_radios) AS radios;