Files
shopdb-flask/plugins/geenforce/parity.py
cproudlock 0dcd186820
All checks were successful
CI / backend (push) Successful in 1m43s
CI / naming (push) Successful in 1s
CI / frontend (push) Successful in 8s
Fix defects found in session review of GE-Enforce plugin
Consolidated fixes from a three-dimension adversarial review.

Data-loss (HIGH): the manifest entry editor stripped fields the form did not
expose, because PUT /entries is a full reset-then-apply. The form now captures
everything - InUseCheck processes as structured name/ExePath/timeout rows (not
just names), LogFile, and the three preinstall flags as checkboxes; the dead
payload-source control (never wired) is removed. New regression test proves an
edit preserves ExePath/timeout/LogFile/PreEnrollment/PCTypesStrict.

Update-entry crash (found by that regression test): replacing an entry's
one-to-one InUseCheck (unique entryid) collided with the old row mid-flush ->
IntegrityError -> 400. update_entry now frees the old InUseCheck (delete+flush)
before populate re-inserts it.

Export truncation (MEDIUM): export_scope_to_share used a plain truncating open,
so a failed/partial write left the live on-share manifest (every PC reads it)
empty. Now writes a temp file in the same dir and os.replace() atomically.

Report dedup case bug (MEDIUM, confirmed by scratch test): the iscurrent demote
matched hostname case-sensitively while the read path uses ilike, so a PC
reporting different casing left two iscurrent rows and double-counted. Demote is
now case-insensitive; regression test added.

Simulator fidelity (MEDIUM): PCTypesStrict was captured but ignored by the
filter mirror, so the simulator wrongly matched a collections-only strict entry
to a nocollections PC via the shared Standard alias group. matches_pctype now
honors PCTypesStrict (disables alias expansion); test added.

Hardening: removed the dead/unscoped GEENFORCE_API_KEY env fallback (never wired
into config; tokens are the only path); create/update entry return 400 on a
duplicate Name instead of 500; parity now asserts scope-level Version/Site; a
new test guards real-manifest field lengths against column limits (the DB-free
parity harness can't see truncation); error handling added to the previously
unguarded editor + reports API calls.

Full suite green; naming + frontend build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 20:46:09 -04:00

105 lines
4.0 KiB
Python

"""P1 behavioral-parity harness (Gate A).
Proves the model is lossless WITHOUT byte-diffing: for each manifest, import it
into in-memory rows and render it back out, then check that
1. every entry is field-for-field identical (engine-relevant fields only,
dropping _comment and key order), in the same order, AND
2. the same entries fire in the same order for a set of machine-profile
fixtures, using the same filter logic the engine uses (filters.py).
Runs with no database (build_scope produces detached ORM objects whose
relationship lists are read directly), so it is cheap to re-run and wrap in CI.
Output is one readable line per scope.
"""
import json
import os
from .importer import build_scope
from .serializer import scope_to_manifest, canonical_entry
from .filters import applicable_entry_names
_FIXTURES_PATH = os.path.join(os.path.dirname(__file__), 'parityfixtures.json')
def load_fixtures():
with open(_FIXTURES_PATH) as handle:
return json.load(handle)
def check_scope(scopename, phase, original, fixtures):
"""Compare an original manifest against its import+export round-trip."""
rebuilt = scope_to_manifest(build_scope(scopename, phase, original))
orig_apps = original.get('Applications') or []
rebuilt_apps = rebuilt.get('Applications') or []
# Check 0: scope-level fields (Version, Site) round-trip.
firstdiff = None
for key in ('Version', 'Site'):
if str(original.get(key) or '') != str(rebuilt.get(key) or ''):
firstdiff = (f'scope field {key}: '
f'{original.get(key)!r} vs {rebuilt.get(key)!r}')
break
scope_ok = firstdiff is None
# Check 1: field-identical, order-preserving.
identical = 0
if firstdiff is None and len(orig_apps) != len(rebuilt_apps):
firstdiff = (f'entry count {len(orig_apps)} vs {len(rebuilt_apps)}')
for i in range(min(len(orig_apps), len(rebuilt_apps))):
co = canonical_entry(orig_apps[i])
cr = canonical_entry(rebuilt_apps[i])
if co == cr:
identical += 1
elif firstdiff is None:
name = orig_apps[i].get('Name', f'#{i}')
diffkeys = sorted(
k for k in set(co) | set(cr) if co.get(k) != cr.get(k))
firstdiff = f'entry "{name}" differs on {diffkeys}'
# Check 2: same entries fire in the same order for each profile.
profiles_same = 0
for profile in fixtures:
if (applicable_entry_names(orig_apps, profile)
== applicable_entry_names(rebuilt_apps, profile)):
profiles_same += 1
elif firstdiff is None:
firstdiff = f'filter mismatch for profile {profile.get("label")}'
passed = (scope_ok
and identical == len(orig_apps) == len(rebuilt_apps)
and profiles_same == len(fixtures))
return {
'scopename': scopename,
'phase': phase,
'entries_total': len(orig_apps),
'entries_identical': identical,
'profiles_total': len(fixtures),
'profiles_same': profiles_same,
'passed': passed,
'firstdiff': firstdiff,
}
def run_parity(manifests, fixtures=None):
"""Run parity over a list of (scopename, phase, manifest_dict). Returns
(results, all_passed)."""
fixtures = fixtures if fixtures is not None else load_fixtures()
results = [check_scope(name, phase, manifest, fixtures)
for name, phase, manifest in manifests]
return results, all(r['passed'] for r in results)
def format_result(result):
"""One human-readable line for a scope result."""
status = 'PASS' if result['passed'] else 'FAIL'
line = (f"scope {result['scopename']:<28} "
f"entries {result['entries_identical']}/{result['entries_total']} identical "
f"profiles {result['profiles_same']}/{result['profiles_total']} same-fire "
f"{status}")
if not result['passed'] and result['firstdiff']:
line += f"\n -> {result['firstdiff']}"
return line