Add curated manifest-entry -> Application link (honest app tracking)
All checks were successful
CI / backend (push) Successful in 1m34s
CI / naming (push) Successful in 1s
CI / frontend (push) Successful in 7s

The honest replacement for the backed-out auto-seeding: instead of scraping
manifest labels into duplicate Application rows, an entry can be LINKED to an
existing catalog Application, cross-referencing what shopdb already tracks.

- Model: manifestentries.appid (nullable soft ref to core applications; in the
  0001 baseline). It is shopdb METADATA, deliberately NOT a manifest field - it
  never appears in the rendered manifest JSON, so enforcement + parity are
  unaffected (test asserts it stays out of the preview manifest).
- API: _entry_payload returns appid + resolved appname; create/update accept an
  optional appid (validated, unknown id ignored, null unlinks) via _apply_app_link;
  GET /geenforce/applications is the picker source (id + name).
- Editor: a "Tracked application (optional)" select in the entry modal, and the
  entry summary line notes the linked app ("...; tracked: eDNC").
- Foundation for a future desired-vs-observed compliance view.

889 tests green (incl. the link test + parity/migration unaffected); build +
naming green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-07-13 06:45:10 -04:00
parent 810687953f
commit 3355436fcd
5 changed files with 100 additions and 5 deletions

View File

@@ -108,6 +108,42 @@ def test_entry_edit_preserves_full_fidelity(client, db, auth_headers):
assert proc['GracefulCloseTimeoutSec'] == 15
def test_entry_curated_app_link(client, db, auth_headers):
"""An entry can be linked to an existing Application; the link is shopdb
metadata and must NOT leak into the manifest JSON (parity/enforcement)."""
from shopdb.core.models import Application
app_row = Application(appname='eDNC (tracked)')
db.session.add(app_row)
db.session.commit()
appid = app_row.appid
scopeid = _create_scope(client, auth_headers)
created = client.post(f'/api/geenforce/scopes/{scopeid}/entries',
json={'Name': 'eDNC install', 'Type': 'MSI',
'appid': appid}, headers=auth_headers)
assert created.status_code == 201, created.get_json()
data = created.get_json()['data']
assert data['appid'] == appid
assert data['appname'] == 'eDNC (tracked)'
# the link is NOT in the rendered manifest (would break the engine contract)
manifest = client.get(f'/api/geenforce/scopes/{scopeid}/preview',
headers=auth_headers).get_json()['data']['manifest']
entry_json = manifest['Applications'][0]
assert 'appid' not in entry_json and 'appname' not in entry_json
# unlink
updated = client.put(f"/api/geenforce/entries/{data['entryid']}",
json={'Name': 'eDNC install', 'Type': 'MSI', 'appid': None},
headers=auth_headers)
assert updated.get_json()['data']['appid'] is None
# picker lists the app
picker = client.get('/api/geenforce/applications',
headers=auth_headers).get_json()['data']
assert any(a['appname'] == 'eDNC (tracked)' for a in picker)
def test_invalid_entry_type_rejected(client, db, auth_headers):
scopeid = _create_scope(client, auth_headers)
resp = client.post(f'/api/geenforce/scopes/{scopeid}/entries',