Files
Classroom_Desktop/tests/test-classroom-wonderink-workspace.sh
T
ekklesiaandClaude Opus 5.5 b5d9535467 feat: Wonder Ink brings back recent tabs for two hours, never the sign-in
A teacher who closes Wonder Ink by accident, or whose computer crashes or
restarts, gets their recent tabs back if they open it again within two
hours of last using it. Later than that — overnight, next week — Wonder
Ink starts fresh and the file is removed.

It is a temporary workspace restore, not a sign-in. Measured on the real
site: WonderInk's rrf-auth cookie is "Basic " and the base64 of the
teacher's email and password, and its start-up authenticates again on
every load; keeping a sign-in would mean keeping a password. Nothing of
it is kept — no cookie, header or storage — and closing Wonder Ink still
signs the teacher out. What is kept is each tab's kind, address or
worksheet file, and title; the order; the tab in front; the time of last
use. An address with a credential in it is not kept at all.

Every WonderInk page has one address, so its tabs come back as one, at
the sign-in page. Its resource files are public and come back as they
were. A worksheet comes back only from Downloads/Wonder Ink, and only if
it is still there and still a PDF. The tab in front loads at once, the
rest when first shown: measured, a PDF loaded in a hidden tab showed at
10%.

Using it is the teacher's input — click, key, wheel, touch — never a
page's own activity, a print preview left open, or the restore itself,
so reopening cannot keep a workspace alive. A restore that crashes twice
is abandoned. Written atomically when tabs change and at most every 30 s
for the time of use; unreadable, future-format and clock-gone-back files
start fresh. The keeper stops filtering events at aboutToQuit: left in
place it crashed Wonder Ink on exit one run in several; 30 runs, none.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-26 21:12:17 -05:00

259 lines
14 KiB
Bash
Executable File

#!/bin/bash
# Wonder Ink's temporary workspace restore: which tabs come back, and when.
#
# lib/classroom_wonderink_workspace.py decides, from a file and the clock,
# whether a teacher's recent tabs are opened again, and which of them may
# be. Every way that could go wrong is either the next teacher meeting the
# last one's tabs, a credential written to disk, or a file Wonder Ink was
# never meant to open — so each is tested here with a clock the test
# controls. Whether the engine really restores them is
# tools/lab/wonderink-workspace-check's question.
set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
WORK="$WORK" PYTHONPATH="$ROOT/lib" python3 - "$ROOT" <<'PY'
import base64
import json
import os
import stat
import sys
from pathlib import Path
import classroom_wonderink_workspace as ws
root = Path(sys.argv[1])
work = Path(os.environ["WORK"])
passed = failed = 0
def check(label, condition, detail=""):
global passed, failed
if condition:
print(f" ok {label}")
passed += 1
else:
print(f" FAIL {label}")
if detail:
print(f" {detail}")
failed += 1
HOUR = 60 * 60
NOW = 1_790_000_000.0
downloads = work / "Downloads" / "Wonder Ink"
downloads.mkdir(parents=True)
sheet = downloads / "Colour the Ark.pdf"
sheet.write_bytes(b"%PDF-1.4 worksheet")
path = work / "wonderink-workspace.json"
SITE = "https://app.wonderink.org/?"
SLIDE = "https://dcc-sbbd-user-content.s3.amazonaws.com/5ee6/69fd-SaulWelcomeQuestionSlide.png"
VIDEO = "https://player.vimeo.com/video/1144568454"
THREE = [ws.tab("web", SITE, "Welcome Question"), ws.tab("web", SLIDE, "Slide"), ws.tab("web", VIDEO, "Video")]
print("1. how long")
check("two hours by default", ws.idle_limit({}) == 2 * HOUR)
check("the test override shortens it", ws.idle_limit({ws.TEST_OVERRIDE: "120"}) == 120)
check("but never below ten seconds", ws.idle_limit({ws.TEST_OVERRIDE: "1"}) == ws.TEST_SHORTEST)
check("and never lengthens it", ws.idle_limit({ws.TEST_OVERRIDE: "999999"}) == 2 * HOUR)
check("nonsense in it is the default", ws.idle_limit({ws.TEST_OVERRIDE: "soon"}) == 2 * HOUR)
check("and it says it is one", ws.is_test_limit({ws.TEST_OVERRIDE: "120"}) and not ws.is_test_limit({}))
print()
print("2. A. restore within the window")
def keep(tabs=THREE, active=1, last=NOW, attempts=0):
ws.write(path, tabs, active, last, attempts)
for label, minutes in (("five minutes", 5), ("ninety minutes", 90), ("a second before two hours", 120 - 1 / 60)):
keep()
data, why = ws.read(path, now=NOW + minutes * 60)
check(f"{label} later: restored", data is not None and len(data["tabs"]) == 3, why)
check("with the time of the last use, not the time it was opened again", data["last_activity"] == NOW)
print()
print("3. B. expiration")
for label, seconds in (("two hours and a minute", 2 * HOUR + 60), ("the next day", 24 * HOUR),
("the next week", 7 * 24 * HOUR)):
keep()
data, why = ws.read(path, now=NOW + seconds)
check(f"{label} later: fresh ({why})", data is None and why == "expired")
check("and the file is gone", not path.exists())
keep(last=NOW)
data, why = ws.read(path, now=NOW + 30, limit=20)
check("the test override applies to a kept workspace", data is None and why == "expired", why)
keep(last=NOW)
data, why = ws.read(path, now=NOW - 2 * HOUR)
check("a clock gone back two hours: fresh, not a workspace made young", data is None and "clock" in why, why)
keep(last=NOW)
data, why = ws.read(path, now=NOW - 30)
check("a clock corrected by half a minute: still restored", data is not None, why)
print()
print("4. D. which tabs, in which order, which in front")
keep()
data, _ = ws.read(path, now=NOW + 60)
check("the order is kept", [t["where"] for t in data["tabs"]] == [SITE, SLIDE, VIDEO])
check("and the tab in front", data["active"] == 1)
check("and readable titles", [t["title"] for t in data["tabs"]] == ["Welcome Question", "Slide", "Video"])
tabs, active = ws.plan(THREE, 1, downloads)
check("all three can be opened, the middle one in front", len(tabs) == 3 and active == 1)
five_site = [ws.tab("web", SITE, f"Lesson {n}") for n in range(3)] + [ws.tab("web", SLIDE, "Slide"),
ws.tab("web", "https://app.wonderink.org/", "Home")]
tabs, active = ws.plan(five_site, 4, downloads)
check("WonderInk's own tabs, which share one address, become one sign-in page, not four",
[t["where"] for t in tabs] == [SITE, SLIDE], tabs)
check("and a WonderInk tab in front brings its one tab to the front", active == 0)
tabs, active = ws.plan(five_site, 3, downloads)
check("the slide in front stays in front", active == 1)
with_sheet = [ws.tab("web", SITE, "Lesson"), ws.tab("worksheet", str(sheet), "Colour the Ark"), ws.tab("web", SLIDE, "Slide")]
tabs, active = ws.plan(with_sheet, 1, downloads)
check("a worksheet that still exists comes back, where it was and named", tabs[1] == ws.tab("worksheet", str(sheet), "Colour the Ark")
and active == 1, tabs)
gone = downloads / "Gone.pdf"
tabs, active = ws.plan([ws.tab("web", SITE), ws.tab("worksheet", str(gone), "Gone"), ws.tab("web", SLIDE)], 1, downloads)
check("a worksheet whose file has gone is left out, silently", [t["where"] for t in tabs] == [SITE, SLIDE], tabs)
check("and the tab in front becomes the one before it", active == 0)
print()
print("5. only files Wonder Ink downloaded, and only PDFs")
elsewhere = work / "secret.pdf"
elsewhere.write_bytes(b"%PDF-1.4 not a worksheet")
(downloads / "sub").mkdir()
(downloads / "sub" / "deep.pdf").write_bytes(b"%PDF-1.4")
notpdf = downloads / "notes.pdf"
notpdf.write_text("<html>an error page</html>")
link = downloads / "link.pdf"
link.symlink_to(elsewhere)
for label, target in (("a file outside Downloads/Wonder Ink", elsewhere), ("a system file", Path("/etc/passwd")),
("a file in a folder inside it", downloads / "sub" / "deep.pdf"),
("a file that is not a PDF", notpdf), ("a link out of it", link),
("a path climbing out of it", downloads / ".." / ".." / "secret.pdf")):
tabs, _ = ws.plan([ws.tab("worksheet", str(target))], 0, downloads)
check(f"{label}: never opened", tabs == [], tabs)
print()
print("6. only http and https, and no credentials")
for label, url in (("a file: address", "file:///etc/passwd"), ("javascript:", "javascript:alert(1)"),
("data:", "data:text/html,hi"), ("blob:", "blob:https://app.wonderink.org/abc"),
("chrome:", "chrome://settings"), ("about:blank", "about:blank"),
("a user and password in it", "https://teacher:[email protected]/"),
("a token", "https://example.org/v?token=abc"), ("an access_token", "https://example.org/v?access_token=abc"),
("a password", "https://example.org/?password=abc"), ("an S3 signature", "https://b.s3.amazonaws.com/x.png?X-Amz-Signature=abc"),
("a key", "https://example.org/?api_key=abc"), ("a session", "https://example.org/?sessionid=abc")):
check(f"{label}: not kept", not ws.keepable_url(url))
check("an ordinary lesson resource is", ws.keepable_url(SLIDE) and ws.keepable_url(VIDEO + "?h=91f1770ec7"))
print()
print("7. F. privacy: what is on disk")
basic = "Basic " + base64.b64encode(b"[email protected]:hunter2hunter2").decode()
risky = THREE + [ws.tab("web", "https://teacher:[email protected]/", "creds"),
ws.tab("web", "https://example.org/?token=" + basic, "token")]
ws.write(path, risky, 0, NOW)
text = path.read_text()
data = json.loads(text)
check("only what may be opened again is written", len(data["tabs"]) == 3, data["tabs"])
check("no password, Basic-auth credential or token in the file",
"hunter2" not in text and "Basic " not in text and "token" not in text.lower() and "@" not in text, text)
check("no cookie, header or storage of any kind",
not any(k in text.lower() for k in ("cookie", "rrf-auth", "x-rrf", "authorization", "localstorage", "sessionstorage")))
check("exactly these fields, and no others",
set(data) == {"format", "last_activity", "active", "attempts", "tabs"}
and all(set(t) == {"kind", "where", "title"} for t in data["tabs"]), data)
check("readable by the classroom account only", stat.S_IMODE(path.stat().st_mode) == 0o600)
check("no half-written file is left beside it", not path.with_name(path.name + ".new").exists())
check("a workspace nobody has used (no time of last use) is not written",
not ws.write(path, THREE, 0, None) and not path.exists())
check("nor one with nothing to open", not ws.write(path, [ws.tab("web", "about:blank")], 0, NOW) and not path.exists())
print()
print("8. E. failure: always a fresh start, never a crash")
for label, content in (("an empty file", ""), ("not JSON", "{not json"), ("JSON of the wrong shape", "[1, 2]"),
("no time of last use", json.dumps({"format": 1, "tabs": THREE, "active": 0})),
("a time that is not a number", json.dumps({"format": 1, "last_activity": "soon", "tabs": THREE})),
("no tabs", json.dumps({"format": 1, "last_activity": NOW, "active": 0, "tabs": []})),
("a future format", json.dumps({"format": 2, "last_activity": NOW, "active": 0, "tabs": THREE})),
("tabs that are not a list", json.dumps({"format": 1, "last_activity": NOW, "tabs": 7}))):
path.write_text(content)
data, why = ws.read(path, now=NOW + 5)
check(f"{label}: fresh ({why}), and removed", data is None and not path.exists(), why)
tabs, _ = ws.plan([{"kind": "web"}, "not a tab", {"kind": "program", "where": "/bin/sh"}, ws.tab("web", SLIDE)], 0, downloads)
check("tabs that make no sense are skipped, the rest kept", [t["where"] for t in tabs] == [SLIDE], tabs)
path.mkdir()
data, why = ws.read(path, now=NOW)
check("a directory where the file should be: fresh, and no crash", data is None, why)
path.rmdir()
(path.with_name(path.name + ".new")).write_text('{"format": 1, "last_act')
keep()
data, why = ws.read(path, now=NOW + 5)
check("a write cut off half way (the .new file) leaves the last whole one", data is not None, why)
print()
print("9. E. a restore that crashes Wonder Ink")
keep(attempts=1)
data, why = ws.read(path, now=NOW + 60)
check("started from once without settling: tried once more", data is not None and data["attempts"] == 1, why)
keep(attempts=2)
data, why = ws.read(path, now=NOW + 60)
check("twice: abandoned, and Wonder Ink starts fresh", data is None and "failed to restore" in why, why)
check("and the workspace is gone", not path.exists())
print()
print("10. Wonder Ink keeps it, and only as this says")
ink = (root / "scripts/classroom-wonderink").read_text()
code = "\n".join(line for line in ink.splitlines() if not line.lstrip().startswith("#"))
keeper = code[code.index("class WorkspaceKeeper"):code.index("class LessonPage")]
check("the engine still keeps no cookie", "PersistentCookiesPolicy.NoPersistentCookies" in code)
check("the keeper never touches a cookie, a header or a page's storage",
not any(w in keeper for w in ("cookieStore", "QNetworkCookie", "setHttpHeader", "localStorage", "runJavaScript")))
check("nothing else in Wonder Ink writes a cookie down", "toRawForm" not in code and "cookieStore()" not in code)
check("activity is the teacher's input: button, key, wheel, touch — not the pointer moving",
all(f"QEvent.Type.{t}" in keeper for t in ("MouseButtonPress", "KeyPress", "Wheel", "TouchBegin"))
and "MouseMove" not in keeper)
check("a restore carries the old time of last use over, not now",
'self._last_wall = kept["last_activity"]' in keeper)
check("a restore is counted in the file before any tab loads",
keeper.index("self._attempts = kept[\"attempts\"] + 1") < keeper.index("for item in tabs:")
and "workspace.write(self._path, tabs, active, self._last_wall, self._attempts)" in keeper)
check("a restored tab loads when it is first shown, the one in front at once (a hidden PDF opened at 10%)",
"self._window._pending[view] = (lambda v=view, u=where: v.load(u))" in keeper
and "pending = self._pending.pop(view, None)" in code)
check("and until then it is kept by its saved address, not dropped as an empty tab",
"if view in self._window._pending:\n tabs.append(self._pending_tabs[view])" in keeper)
check("and the count cleared once Wonder Ink has settled", "QTimer.singleShot(self.SETTLED_MS, self._settled)" in keeper)
check("the tabs are written when they change, not only at the end",
"view.urlChanged.connect(lambda *_: self._workspace_changed())" in code
and "tabMoved.connect(lambda *_: self._workspace_changed())" in code
and "currentChanged.connect(lambda *_: self._workspace_changed())" in code)
check("a page changing its own tab is not activity", "urlChanged.connect(lambda *_: self._workspace_changed())" in code
and "touched()" not in code[code.index("def _workspace_changed"):code.index("def _workspace_changed") + 200])
check("at quit it keeps the tabs, then stops filtering events",
"aboutToQuit.connect(self.shut_down)" in keeper and "removeEventFilter(self)" in keeper)
check("worksheet downloads are connected before the workspace opens",
code.index("profile.downloadRequested.connect(window.on_download)\n open_workspace(window)") > 0)
setters = [str(p.relative_to(root)) for top in ("config", "packaging", "systemd", "scripts")
for p in (root / top).rglob("*") if p.is_file() and not p.is_symlink()
and not str(p).startswith(str(root / "packaging" / "root"))
and ws.TEST_OVERRIDE in p.read_text(errors="ignore")]
check("nothing the classroom installs sets the test-only override", setters == [], setters)
check("and Wonder Ink says so in the journal when it is set", "TEST ONLY" in code and "workspace.is_test_limit()" in code)
check("the module is packaged", "lib/classroom_wonderink_workspace.py" in (root / "tools/build-release.sh").read_text())
print()
print(f"{passed} passed, {failed} failed")
sys.exit(1 if failed else 0)
PY