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>
193 lines
8.3 KiB
Python
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())
|