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>
This commit is contained in:
192
tests/usage_check.py
Normal file
192
tests/usage_check.py
Normal file
@@ -0,0 +1,192 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user