Add GE-Enforce export-to-share + fleet-compliance UI (Milestone 1 UX)
Rounds out the Milestone 1 admin experience: author + publish in shopdb, push to the share by a button, and see what the fleet actually did. Export to share: - GET/PUT /api/geenforce/config stores the on-share export root (Setting geenforce_share_root); POST /scopes/<id>/export-share writes the current published JSON to <shareroot>/<scope>/manifest.json (preinstall.json for the preinstall phase), backing up the existing file to _meta/history first. geenforce.publish gated. The engine and PCs are untouched - this is the safe Milestone 1 push whose rollback is restoring the history backup. - Editor: a share-root config row + an "Export to Share" button per scope. - 3 tests (config roundtrip, export writes the file, second export backs up). Fleet-compliance UI (Settings > Enforcement Reports): - New page over GET /reports + /reports/<id>: latest report per PC with received (applied vs latest published version), status (ok/selfhealed/failed), and install/skip/fail counts; row detail shows per-entry outcomes with self-heal flags, exit codes, and messages. Hostname/PC-type filters. - ADR-010 settings card + ADR-009 plugin-gated route. Full suite 883 green; frontend build + naming green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
65
tests/test_plugins/test_geenforce_export.py
Normal file
65
tests/test_plugins/test_geenforce_export.py
Normal file
@@ -0,0 +1,65 @@
|
||||
"""GE-Enforce export-to-share: config the share root, publish, write to share.
|
||||
|
||||
This is the Milestone 1 loop - author + publish in shopdb, then export the
|
||||
published JSON to the on-share manifest with a history backup, engine untouched.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
from plugins.geenforce import service
|
||||
|
||||
|
||||
def _scope_with_entry(app, name='gea-shopfloor-cmm'):
|
||||
with app.app_context():
|
||||
service.replace_scope_draft(name, 'runtime', {
|
||||
'Version': '2.6',
|
||||
'Applications': [{'Name': 'Alpha', 'Type': 'MSI',
|
||||
'Installer': 'apps/a.msi'}]})
|
||||
service.publish_scope(name, 'runtime', notes='v1')
|
||||
scope = service.ManifestScope.query.filter_by(scopename=name).first()
|
||||
scopeid = scope.scopeid
|
||||
service.db.session.commit()
|
||||
return scopeid
|
||||
|
||||
|
||||
def test_export_requires_config(client, db, app, auth_headers):
|
||||
scopeid = _scope_with_entry(app)
|
||||
resp = client.post(f'/api/geenforce/scopes/{scopeid}/export-share',
|
||||
headers=auth_headers)
|
||||
assert resp.status_code == 400 # no share root configured
|
||||
|
||||
|
||||
def test_config_roundtrip_and_export(client, db, app, auth_headers, tmp_path):
|
||||
scopeid = _scope_with_entry(app)
|
||||
|
||||
# Configure the share root.
|
||||
put = client.put('/api/geenforce/config',
|
||||
json={'shareroot': str(tmp_path)}, headers=auth_headers)
|
||||
assert put.status_code == 200
|
||||
got = client.get('/api/geenforce/config', headers=auth_headers)
|
||||
assert got.get_json()['data']['shareroot'] == str(tmp_path)
|
||||
|
||||
# Export writes the published JSON to <shareroot>/<scope>/manifest.json.
|
||||
resp = client.post(f'/api/geenforce/scopes/{scopeid}/export-share',
|
||||
headers=auth_headers)
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
path = resp.get_json()['data']['path']
|
||||
assert os.path.isfile(path)
|
||||
written = json.load(open(path))
|
||||
assert [e['Name'] for e in written['Applications']] == ['Alpha']
|
||||
|
||||
|
||||
def test_export_backs_up_existing(client, db, app, auth_headers, tmp_path):
|
||||
scopeid = _scope_with_entry(app)
|
||||
client.put('/api/geenforce/config', json={'shareroot': str(tmp_path)},
|
||||
headers=auth_headers)
|
||||
# First export creates the file.
|
||||
client.post(f'/api/geenforce/scopes/{scopeid}/export-share',
|
||||
headers=auth_headers)
|
||||
# Second export backs the old file into _meta/history.
|
||||
client.post(f'/api/geenforce/scopes/{scopeid}/export-share',
|
||||
headers=auth_headers)
|
||||
historydir = tmp_path / '_meta' / 'history'
|
||||
assert historydir.is_dir()
|
||||
assert any(historydir.iterdir())
|
||||
Reference in New Issue
Block a user