fix: keep mesh/cluster links so edges resolve onto a second canvas
Approving the same zigbee/zwave/proxmox devices onto a second design placed the nodes but drew no edges. _resolve_pending_links_for_ieee deleted each pending_device_link after materializing its edge, so the first approve consumed the whole topology and later approves had nothing to resolve. Links are topology, not one-shot: every importer wipes+reinserts its link set on each import, so they can safely persist across approvals. - keep the link rows (drop both db.delete(link) calls) - scope resolution to the target design (Node.ieee_address + design_id) so a re-approve links that canvas's nodes, not another's - thread design_id through all three approve call sites Existing links deleted by the old code do not come back on their own; a re-import repopulates them. ha-relevant: maybe
This commit is contained in:
@@ -389,7 +389,7 @@ async def test_cluster_link_resolves_to_cluster_edge(db_session) -> None:
|
||||
))
|
||||
await db_session.commit()
|
||||
|
||||
await _resolve_pending_links_for_ieee(db_session, "pve-node-a")
|
||||
await _resolve_pending_links_for_ieee(db_session, "pve-node-a", design.id)
|
||||
|
||||
edge = (await db_session.execute(select(Edge))).scalars().one()
|
||||
assert edge.type == "cluster"
|
||||
@@ -399,6 +399,42 @@ async def test_cluster_link_resolves_to_cluster_edge(db_session) -> None:
|
||||
assert edge.target_handle == "left"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_link_survives_and_resolves_onto_second_design(db_session) -> None:
|
||||
# Regression: approving the same mesh devices onto a second canvas must also
|
||||
# get their edges. The link row is topology, not one-shot — resolving it on
|
||||
# design A must not consume it, so design B resolves too.
|
||||
da = Design(id=str(uuid.uuid4()), name="a")
|
||||
db_ = Design(id=str(uuid.uuid4()), name="b")
|
||||
db_session.add_all([da, db_])
|
||||
# Same two devices placed on BOTH designs (one Node per canvas).
|
||||
for d in (da, db_):
|
||||
db_session.add_all([
|
||||
Node(id=str(uuid.uuid4()), type="iot", label="x", ieee_address="0xAAAA",
|
||||
status="online", pos_x=0, pos_y=0, design_id=d.id),
|
||||
Node(id=str(uuid.uuid4()), type="iot", label="y", ieee_address="0xBBBB",
|
||||
status="online", pos_x=0, pos_y=0, design_id=d.id),
|
||||
])
|
||||
db_session.add(PendingDeviceLink(
|
||||
id=str(uuid.uuid4()), source_ieee="0xAAAA", target_ieee="0xBBBB",
|
||||
discovery_source="zigbee",
|
||||
))
|
||||
await db_session.commit()
|
||||
|
||||
first = await _resolve_pending_links_for_ieee(db_session, "0xAAAA", da.id)
|
||||
second = await _resolve_pending_links_for_ieee(db_session, "0xAAAA", db_.id)
|
||||
|
||||
assert len(first) == 1
|
||||
assert len(second) == 1 # would be 0 if the link were consumed on design A
|
||||
# One iot edge per design, each between that design's own nodes.
|
||||
edges = (await db_session.execute(select(Edge))).scalars().all()
|
||||
assert len(edges) == 2
|
||||
assert {e.design_id for e in edges} == {da.id, db_.id}
|
||||
assert all(e.type == "iot" for e in edges)
|
||||
# The link row is still present for any further design.
|
||||
assert (await db_session.execute(select(PendingDeviceLink))).scalars().one()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_persist_never_deletes(db_session) -> None:
|
||||
await _persist_pending_import(db_session, [_guest_node(101, "10.0.0.5")], [])
|
||||
|
||||
@@ -1375,7 +1375,9 @@ async def test_approve_zigbee_skips_when_other_endpoint_still_pending(
|
||||
async def test_approve_zigbee_resolves_link_after_second_approval(
|
||||
client: AsyncClient, headers, db_session
|
||||
):
|
||||
"""First approval keeps link; second approval creates the edge."""
|
||||
"""First approval keeps link (other endpoint pending); second approval
|
||||
creates the edge. The link row is retained afterwards so the same pair can
|
||||
be re-approved onto another canvas — it's topology, wiped only on reimport."""
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.db.models import Edge, PendingDevice, PendingDeviceLink
|
||||
@@ -1408,7 +1410,7 @@ async def test_approve_zigbee_resolves_link_after_second_approval(
|
||||
edges = (await db_session.execute(select(Edge))).scalars().all()
|
||||
assert len(edges) == 1
|
||||
links = (await db_session.execute(select(PendingDeviceLink))).scalars().all()
|
||||
assert links == [] # consumed
|
||||
assert len(links) == 1 # retained for re-approval onto other canvases
|
||||
|
||||
|
||||
# --- Deep scan: trigger overrides + config persistence ---
|
||||
|
||||
Reference in New Issue
Block a user