Files
Project-SDE-WP-Suite/tests/usage_check.py
n.siegfried 82a8f30074 T7.10 - D5: one analytics implementation, and its report on the admin console
Usage analytics existed as five of the nine colliding globals creator-frame.md
counted (ANALYTICS_KEY, analyticsLoad, analyticsSave, downloadAnalytics,
showAnalytics), twice - and the wizard's copy had no caller, because the
button lived on the creator. The admin console had a THIRD private reader
(usageLoad/downloadUsage) that only saw the wizard's key.

Now: ONE core, html/wp-usage.js (window.WPUsage: load/save/track/download +
the two pre-move storage keys, verbatim). The creator and wizard keep only a
thin track() wrapper - page state like the creator's dev-mode pause belongs
to the page - and record exactly what they recorded before, under the same
keys, so everything captured before this task still reads (probe plants a
legacy-format event and finds it in the report). The "Usage data" button left
the creator toolbar; the report lives in admin.html's usage card, covering
BOTH tools with a download each, behind the same admin gate as the rest of
the console (a non-admin sees the denied card and nothing else), usable at
390px.

Two probes re-pointed, both with the reason in the code:
- cards_check pinned admin.js byte-identical to HEAD - right for T6.5, but as
  a standing probe it would fail every legitimate later edit; D5 targets
  admin.js by name. A7's localization is protected by the feature checks and
  the end-to-end drive, plus a wiring assertion on the block itself.
- frame_check listed "Usage data" among the toolbar buttons that must be
  visible; it now asserts the button is GONE, so the duplicate cannot quietly
  return.

Verification (each probe run alone): NEW tests/usage_check.py 15/15 (grep
half: WPUsage defined once, no page touches the keys directly, none of the
five globals survives anywhere). Regressions: cards_check ALL PASS,
frame_check 39/39.

Items: D5

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 11:29:27 -07:00

193 lines
8.3 KiB
Python

#!/usr/bin/env python3
"""Is there exactly one analytics implementation, reported from admin? — D5, T7.10.
Usage analytics existed twice (creator + wizard), five of the nine colliding
globals `creator-frame.md` counted, and the wizard's copy had no caller. One
core survives in wp-usage.js; the pages keep thin track() wrappers; the report
and its downloads live on the admin console behind the same role gate as the
rest of that page. Keys are unchanged, so pre-move data still reads.
Self-contained: throwaway SQLite, its own uvicorn, headless Edge or Chrome.
Exit 0 all passed, 1 a failure, 2 could not run.
"""
import json
import os
import re
import sys
import tempfile
import time
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import cdp # noqa: E402
from browser_check import seed, start_server, chk, _PASS, _FAIL # noqa: E402
from sections_check import set_sop # noqa: E402
from stepper_check import dismiss_dialogs # noqa: E402
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
HTML = os.path.join(ROOT, "html")
def ascii_(v, n=280):
return re.sub(r"\s+", " ", str(v)).encode("ascii", "replace").decode()[:n]
def settle(seconds=0.6):
time.sleep(seconds)
def strip_js(src):
src = re.sub(r"/\*.*?\*/", "", src, flags=re.S)
return "\n".join(re.sub(r"(?<![:'\"])//.*$", "", ln) for ln in src.split("\n"))
def main():
exe = cdp.find_browser()
if not exe:
print("no headless-capable browser found; set WP_BROWSER.")
return 2
# ── 1. the grep half: one implementation, no leftover controls ───────────
print("\n1. grep: one implementation, nothing unreferenced")
files = {}
for name in os.listdir(HTML):
if name.endswith((".js", ".html")):
files[name] = strip_js(open(os.path.join(HTML, name), encoding="utf-8").read())
core_defs = [n for n, src in files.items() if "window.WPUsage" in src]
chk("the WPUsage core is defined in wp-usage.js and only there",
core_defs == ["wp-usage.js"], ascii_(core_defs))
chk("the pages record THROUGH it - no page touches the storage keys directly",
all("wp_iwp_analytics_v1" not in src and "wp_suite_analytics_v1" not in src
for n, src in files.items()
if n not in ("wp-usage.js",) and n.endswith(".js")))
leftovers = {n: re.findall(r"analyticsLoad|analyticsSave|downloadAnalytics|showAnalytics"
r"|ANALYTICS_KEY|USAGE_KEY|usageLoad|downloadUsage", src)
for n, src in files.items() if n != "wp-usage.js"}
leftovers = {n: v for n, v in leftovers.items() if v}
chk("none of the five colliding globals survives anywhere; grep confirms",
not leftovers, ascii_(leftovers))
chk("no analytics control remains on the creator or the wizard; grep confirms",
"Usage data" not in files["wp-creation-index.html"]
and "showAnalytics" not in files["work-package-suite.html"])
chk("both storage keys survive, verbatim, in the core (data continuity)",
"wp_iwp_analytics_v1" in files["wp-usage.js"]
and "wp_suite_analytics_v1" in files["wp-usage.js"])
tmpdir = tempfile.mkdtemp(prefix="wpsuite-usage-")
db_path = os.path.join(tmpdir, "check.db")
server = None
browser = None
try:
tok = seed(db_path)
set_sop(db_path, {})
port = cdp.free_port()
base = "http://127.0.0.1:%d" % port
server = start_server(port, db_path)
browser = cdp.Browser(exe)
page = browser.page()
page.clear_cookies()
page.set_cookie("wp_session", tok["root"])
page.viewport(1440, 900)
# ── 2. recording still works from both tools ─────────────────────────
print("\n2. the tools still record")
page.goto(base + "/wp-creation-index.html?project=projA")
dismiss_dialogs(page)
settle(2.0)
# Plant a LEGACY-format event under the pre-move key: the done-when is
# that data recorded before this task is still readable after it.
page.eval("""(() => {
const d = JSON.parse(localStorage.getItem('wp_iwp_analytics_v1')) || {events: []};
d.events.unshift({ts: '2026-07-01T10:00:00Z', session: 's_legacy',
event: 'legacy_probe_event', detail: null});
localStorage.setItem('wp_iwp_analytics_v1', JSON.stringify(d));
})()""")
n0 = page.eval("WPUsage.load(WPUsage.KEYS.creator).events.length")
page.eval("track('probe_event')")
chk("the creator's track() still lands events under the old key",
page.eval("WPUsage.load(WPUsage.KEYS.creator).events.length") == n0 + 1)
page.goto(base + "/work-package-suite.html?tab=sop")
dismiss_dialogs(page)
settle(2.0)
n0 = page.eval("WPUsage.load(WPUsage.KEYS.wizard).events.length")
page.eval("track('probe_event_wizard')")
chk("the wizard's track() still lands events under its old key",
page.eval("WPUsage.load(WPUsage.KEYS.wizard).events.length") == n0 + 1)
wiz_errors = [e for e in page.js_errors() if "beforeunload" not in e]
chk("...and the wizard page throws no errors without its old globals "
"(the blocked-beforeunload console line is BL-020, filtered not hidden)",
not wiz_errors, ascii_(wiz_errors[:2]))
# ── 3. the report, on admin, behind the admin gate ───────────────────
print("\n3. the admin report")
page.goto(base + "/admin.html")
dismiss_dialogs(page)
settle(2.0)
page.eval("loadUsage()")
settle(0.5)
report = page.eval("(document.getElementById('usage-admin')||{textContent:''}).textContent")
chk("usage data is reachable from admin.html, both tools reported",
"Work package creator" in report and "SOP wizard" in report, ascii_(report, 160))
chk("the pre-move legacy event is readable in the report",
"legacy_probe_event" in report)
chk("this session's fresh events are in it too",
"probe_event" in report and "probe_event_wizard" in report)
chk("each tool offers its download from the report",
page.eval("[...document.querySelectorAll('#usage-admin button')].length") >= 2)
page.viewport(390, 844, mobile=True)
settle(0.6)
fits = page.eval("""(() => {
const box = document.getElementById('usage-admin');
return box && box.scrollWidth <= box.clientWidth + 2
&& document.documentElement.scrollWidth <= 392;
})()""")
chk("the report is usable at 390px - no sideways scrolling", bool(fits))
page.viewport(1440, 900)
settle(0.4)
# The same role gate as the rest of the console: a non-admin sees the
# denied card and no cards, this one included.
page.clear_cookies()
page.set_cookie("wp_session", tok["pat"])
page.goto(base + "/admin.html")
dismiss_dialogs(page)
settle(2.0)
chk("a non-administrator gets the denied notice, not the usage report",
page.eval("""(() => {
const denied = document.getElementById('admin-denied');
const usage = document.getElementById('usage-admin');
const visible = el => !!el && el.offsetParent !== null;
return visible(denied) && !visible(usage);
})()"""))
js_errors = [e for e in page.js_errors() if "beforeunload" not in e]
chk("no JavaScript errors anywhere in this run", not js_errors,
ascii_(js_errors[:2]))
finally:
if browser is not None:
try:
browser.close()
except Exception:
pass
if server is not None:
try:
server.terminate()
except Exception:
pass
print("\n" + "-" * 54)
print("%d/%d checks passed." % (len(_PASS), len(_PASS) + len(_FAIL)))
for f in _FAIL:
print(" - " + f)
return 1 if _FAIL else 0
if __name__ == "__main__":
sys.exit(main())