Add the dualpath-as-single-machine site toggle
All checks were successful
CI / backend (push) Successful in 1m13s
CI / naming (push) Successful in 1s
CI / frontend (push) Successful in 7s

Most facilities consider a Dualpath pair one physical dual-bay machine.
New site setting dualpath_single_machine (default on): the machines
list, dashboard counts, machines-by-type report, and floor map collapse
each pair to its primary bay (lower assetnumber), with combined
2007 / 2008 labels; pagination totals stay honest. Detail pages remain
per-bay and always show a dual-bay sibling banner linking the partner.
Pair resolution lives in core services and joins the plugin contract
surface (0.8.0 -> 0.9.0).

On the WJ dataset: 31 pairs collapse, machine counts 262 -> 231, map
470 assets. Toggle verified live in both states, left on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-07-12 06:23:39 -04:00
parent 5a192f3100
commit b130ef43f3
17 changed files with 525 additions and 15 deletions

View File

@@ -247,3 +247,49 @@ def test_cli_backfill_propagates_and_is_idempotent(client, db, auth_headers, run
sourceassetid=pc.assetid, targetassetid=bayb.assetid,
relationshiptypeid=controls.relationshiptypeid).count()
assert count == 1
# ---------------------------------------------------------------------------
# CLI fix-controls-direction flips reversed legacy rows to PC -> machine
# ---------------------------------------------------------------------------
def test_cli_fix_controls_direction(client, db, auth_headers, runner):
atype, controls, dualpath, partof = _setup_types()
ctype = AssetType(assettype='computer')
db.session.add(ctype)
db.session.flush()
pc = _make_asset('PC-7', ctype.assettypeid)
pc2 = _make_asset('PC-8', ctype.assettypeid)
bay = _make_asset('BAY-7', atype.assettypeid)
bay2 = _make_asset('BAY-8', atype.assettypeid)
# reversed legacy row: machine -> PC
db.session.add(AssetRelationship(sourceassetid=bay.assetid, targetassetid=pc.assetid,
relationshiptypeid=controls.relationshiptypeid))
# reversed row whose flip already exists -> reversed one gets deactivated
db.session.add(AssetRelationship(sourceassetid=bay2.assetid, targetassetid=pc2.assetid,
relationshiptypeid=controls.relationshiptypeid))
db.session.add(AssetRelationship(sourceassetid=pc2.assetid, targetassetid=bay2.assetid,
relationshiptypeid=controls.relationshiptypeid))
db.session.commit()
result = runner.invoke(args=['relationships', 'fix-controls-direction'])
assert result.exit_code == 0, result.output
assert 'Flipped 1' in result.output
assert '1 reversed duplicate(s) deactivated' in result.output
# first row now reads PC -> machine
assert _rel_exists(pc.assetid, bay.assetid, controls.relationshiptypeid)
assert not _rel_exists(bay.assetid, pc.assetid, controls.relationshiptypeid)
# duplicate pair: canonical row stays active, reversed one deactivated
rev = AssetRelationship.query.filter_by(
sourceassetid=bay2.assetid, targetassetid=pc2.assetid,
relationshiptypeid=controls.relationshiptypeid).first()
assert rev is not None and rev.isactive is False
# idempotent: second run finds nothing
result2 = runner.invoke(args=['relationships', 'fix-controls-direction'])
assert result2.exit_code == 0, result2.output
assert 'Flipped 0' in result2.output
assert '0 reversed duplicate(s) deactivated' in result2.output