computers: declare subordinate devices instead of coding each one
Some checks failed
CI / backend (push) Failing after 8s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 9s
CI / migrations-mysql (push) Failing after 6s

A PC that drives a device which is its own asset had been implemented twice.
METROLOGY_TOOL_MAP covered CMM, Keyence, Genspect and wax-trace, minting a
measuring_tool. A separate path keyed on one hardcoded pc-type minted a Part
Marker machine and filed it under its operation. Both create a device, link the
PC with controls, and archive that link when the PC is re-imaged: one mechanism
with different nouns, written out twice because the second case arrived later.

That is the same trap as the site literals in ADR-015 - a pattern implemented
per instance rather than declared - and it has a known next occurrence. Part
markers already share operation numbers, and any site with two marking lasers
or two wax-trace units on one number needs identical treatment.

One SUBORDINATE_DEVICE_MAP now declares asset type, type name, naming suffix,
whether the device files partof the operation, and the relationship label. The
labels are unchanged per case on purpose: those values are in the production
database and only rows carrying them are archived by a collector push. A site
overrides or adds an entry through subordinatedevice_<pctype> settings, per
ADR-015, so the next case needs no code. A malformed override falls back to the
default rather than failing the push, because a bad setting must not stop a bay
reporting its inventory.

metrology_tool_for stays as a shim over the same map: filters.py and the older
tests read it, and unifying must not change what it returns. A test pins that.

Also adds flask relationships check-shared-machines, which finds the next 0615
rather than waiting for someone to notice duplicate backups. Several devices
legitimately sharing a number and two PCs mis-numbered at imaging look the same
from outside; the difference is whether child assets exist, so that is what it
reports. Read-only.
This commit is contained in:
cproudlock
2026-08-11 11:13:12 -04:00
parent 91143d94fb
commit c90ebcbc7c
5 changed files with 280 additions and 53 deletions

View File

@@ -1046,3 +1046,59 @@ def test_a_marker_is_never_filed_under_its_own_pc(
assert comp.assetid not in targets, 'marker filed under its own PC'
assert any("own asset number" in w
for w in r.get_json()['data']['warnings'])
# =============================================================================
# The subordinate-device map: adding a device type is configuration, not code
# =============================================================================
def test_a_site_can_add_a_device_type_without_a_code_change(
client, db, collector_key, computer_assettype, operation_0615):
"""The point of the map. A site with two marking lasers on one operation
number needs the same treatment part markers got; before this it needed an
edit to the plugin."""
import json
from shopdb.core.models import Setting, Asset
db.session.add(Setting(
key='subordinatedevice_site-laser',
value=json.dumps({'assettype': 'machine', 'typename': 'Marking Laser',
'description': 'Site laser', 'suffix': 'LASER',
'partof': True, 'label': 'collector:partmarker'}),
category='pctypemapping'))
db.session.commit()
client.post('/api/collector/computers',
json={'hostname': 'LASERPC1', 'machinenumber': '0615',
'pctype': 'site-laser'},
headers={'X-API-Key': collector_key})
device = Asset.query.filter_by(assetnumber='LASERPC1-LASER').first()
assert device is not None
assert device.machine.machinetype.machinetype == 'Marking Laser'
def test_a_malformed_override_does_not_break_the_collector(
client, db, collector_key, computer_assettype, operation_0615):
"""A bad setting must not stop a bay reporting its inventory."""
from shopdb.core.models import Setting
db.session.add(Setting(key='subordinatedevice_gea-shopfloor-partmarker',
value='{not json', category='pctypemapping'))
db.session.commit()
r = client.post('/api/collector/computers', json=_marker('MARKERPC9'),
headers={'X-API-Key': collector_key})
assert r.status_code == 200, r.get_json()
def test_the_metrology_map_still_answers_through_the_shim(app):
"""filters.py and the older tests read metrology_tool_for. Unifying the two
maps must not change what it returns."""
with app.app_context():
from plugins.computers.pctypemap import metrology_tool_for
assert metrology_tool_for('gea-shopfloor-cmm') == (
'CMM', 'Coordinate measuring machine')
# A machine-typed device is not a measuring tool.
assert metrology_tool_for('gea-shopfloor-partmarker') is None
assert metrology_tool_for('gea-shopfloor-collections') is None