Files
homelable/backend/tests/test_rackable_sync.py
Pouzor b129200929 feat(rack): pick a mount from the Device Inventory, filtered to rackable gear
The rack device modal listed inventory entries in a flat <select>: one line per
device, no search, no filters, nothing to tell two look-alike hosts apart. It
now opens the Device Inventory modal itself in a picker mode — a card click
returns the device instead of approving it, and the bulk/clear controls (plus
the s/a/Enter shortcuts) are hidden, since a picker has no business deleting
anything.

That list also carried VMs, containers and mesh devices, which no rack can
hold. A new "Rackable" filter drops them, armed by default when the rack opens
the modal. It is an exclusion list, not an allow list, so new hardware types
show up by default and an unclassified device (a bare ARP hit) stays visible.
`utils/rackable.ts` mirrors `_UNRACKABLE_TYPES` in racks.py — the list the
inventory endpoint already filtered on — and test_rackable_sync.py fails the
backend suite on drift.

The rack side keeps the last word: a device missing from its own inventory, or
already mounted in this design, is refused by name rather than mounted as a
ghost. Nothing is preselected anymore either, so submitting without picking
says so instead of quietly mounting whichever entry happened to be first.

Standalone keeps the <select>: the inventory modal reads pending_devices over
REST and there is no backend there.

The modal itself was max-w-md and one column, which pushed the port list below
the fold. It now matches NodeModal's width with the fields on the left and the
ports on the right.

ha-relevant: maybe
2026-08-09 20:35:23 +02:00

35 lines
1.3 KiB
Python

"""The rackable exclusion list is duplicated in the frontend — keep them equal.
`GET /api/v1/racks/inventory` filters on `_UNRACKABLE_TYPES`, while the Device
Inventory modal's "Rackable" filter (used as the rack picker's default) filters
on `UNRACKABLE_TYPES` in `frontend/src/utils/rackable.ts`. If the two drift, the
picker offers devices the rack side then refuses.
"""
import re
from pathlib import Path
from app.api.routes.racks import _UNRACKABLE_TYPES
FRONTEND_FILE = (
Path(__file__).resolve().parents[2] / "frontend" / "src" / "utils" / "rackable.ts"
)
def _frontend_unrackable() -> set[str]:
src = FRONTEND_FILE.read_text()
match = re.search(r"export const UNRACKABLE_TYPES = new Set\(\[(.*?)\]\)", src, re.S)
assert match, (
"Could not locate UNRACKABLE_TYPES in frontend/src/utils/rackable.ts — "
"update this parser if the file's shape changed."
)
return set(re.findall(r"'([^']+)'", match.group(1)))
def test_unrackable_types_in_sync_with_frontend():
frontend = _frontend_unrackable()
assert frontend == _UNRACKABLE_TYPES, (
"racks.py _UNRACKABLE_TYPES is out of sync with frontend rackable.ts.\n"
f"Missing from the backend: {sorted(frontend - _UNRACKABLE_TYPES)}\n"
f"Missing from the frontend: {sorted(_UNRACKABLE_TYPES - frontend)}"
)