"""The T10 views bundle (goal 41 task 10): the two phase-D structural bets — saved views / URL state on /tasks (ux-notes bet 4) and the cross-entity work stream on the dashboard (bet 5). Bet 4 is a CLIENT-side engine (the statum entrypoint fetch carries no query string, so filter state lives in location.hash): this bundle verifies the engine's round-trip as pure logic (a python port of the serialise/parse pair driving the exact page functions) plus the markup hooks the page ships. The prefill namespace (#new?project=) must keep coexisting with the view namespace (#view?...). Bet 5 is server data + markup: the dashboard slot's work_stream rows (joined, time-ordered, capped, linked) are cross-checked against seeded ledger events, and the page renders the stream feed with the old activity table gone. """ from harness import * # noqa: F401,F403 — the shared wire helpers # The open-work status set (window.TASK_OPEN_STATUSES on the page). OPEN = ("queued", "assigned", "working", "blocked", "suspended") def view_hash(filters): """window.taskViewHash ported verbatim: only non-default axes serialise ('open' status, all projects, all goals stay clean).""" parts = [] for key in ("status", "project", "goal"): value = filters.get(key) if key == "status": value = value or "open" if value == "open": continue elif value in (None, "", 0, "0", "all"): continue from urllib.parse import quote parts.append("%s=%s" % (key, quote(str(value), safe=""))) return "#view?" + "&".join(parts) if parts else "" def filters_from_hash(hash_text): """window.taskFiltersFromHash ported verbatim (None = not a view hash: the #new prefill namespace and bare hashes fall through).""" if not hash_text or not hash_text.startswith("#view?"): return None filters = {} for part in hash_text[len("#view?"):].split("&"): if "=" not in part: continue key, value = part.split("=", 1) from urllib.parse import unquote key, value = unquote(key), unquote(value) if key == "status" and value: filters["status"] = value elif key == "project" and value and value != "0": filters["project"] = value elif key == "goal" and value and value != "all": filters["goal"] = value return filters def run(ctx): auth, fake = ctx.auth, ctx.fake now = "strftime('%s','now')" # ------------------------------------------------------------------ print("== bet 4: the hash engine (round-trip as pure logic) ==") cases = [ ({}, ""), # the default view keeps the URL clean ({"status": "open"}, ""), ({"status": "working"}, "#view?status=working"), ({"status": "blocked"}, "#view?status=blocked"), ({"status": "all"}, "#view?status=all"), ({"project": 3}, "#view?project=3"), ({"project": "3"}, "#view?project=3"), ({"project": 0}, ""), # "all projects" is the default ({"goal": "standalone"}, "#view?goal=standalone"), ({"goal": "all"}, ""), ({"goal": 12}, "#view?goal=12"), ({"status": "working", "project": 3, "goal": "standalone"}, "#view?status=working&project=3&goal=standalone"), # A title with spaces/ampersands survives the round-trip # encoded (the goal axis carries ids in practice, but the # engine must not corrupt any value). ({"goal": "a & b=c"}, "#view?goal=a%20%26%20b%3Dc"), ] for filters, expected in cases: got = view_hash(filters) check(got == expected, "serialise %r -> %r" % (filters, expected)) if expected: back = filters_from_hash(expected) wanted = {} for key, value in filters.items(): text = str(value) if key == "status": if text not in ("open", ""): wanted[key] = text elif text not in ("0", "all", ""): wanted[key] = text check(back == wanted, "parse %r restores %r (got %r)" % (expected, wanted, back)) # The prefill namespace never parses as a view. check(filters_from_hash("#new?project=7") is None, "the #new prefill hash is not a view hash") check(filters_from_hash("") is None and filters_from_hash("#") is None, "bare hashes are not view hashes") print("== bet 4: the page ships the engine + the view row ==") tasks_page = http_get_page(auth, "/tasks") for needle, label in ( ("window.taskViewHash", "the serialiser"), ("window.taskFiltersFromHash", "the parser"), ("window.applyTaskViewHash", "the load-time seed"), ("window.copyTaskViewLink", "the copy-link affordance"), ("syncTaskHash", "the filter-change hash write"), ): check(needle in tasks_page, "the tasks page ships %s" % label) check("syncTaskHash();" in tasks_page, "every filter change lands in the hash (setTaskFilter)") check("window.history.replaceState" in tasks_page, "hash updates use replaceState (no Back-button spam)") check("hashchange" in tasks_page, "a pasted #view hash re-seeds the filters in-place") check("window.applyTaskViewHash();" in tasks_page, "the hash seeds taskFilters before the first render") check("#view?" in tasks_page, "the view namespace is distinct from the #new prefill") # The view row: presets + copy button. check('data-task-view-row' in tasks_page, "the view row is marked for the bundle/harness") check("Copy view link" in tasks_page and "copyTaskViewLink" in tasks_page, "the one-click copy-link button ships") for preset in ("Open work", "All", "Blocked"): check(preset in tasks_page, "the %r preset chip ships in the view row" % preset) check("navigator.clipboard" in tasks_page, "the copy uses the async clipboard API") # The presets ARE the chips' statuses (no second filter state). check(tasks_page.count("window.taskChipClass('open')") >= 2, "the presets reuse the chip engine's status vocabulary") print("== bet 4: prefill coexistence ==") # T9's #new?project= deep-link still parses + drops itself, and # the view engine ignores it (namespace check above; markup). check("indexOf('#new')" in tasks_page, "the #new prefill contract still ships (T9)") check("indexOf('#view?')" in tasks_page, "the view parser gates on its own namespace") # A prefill hash and a view hash in one URL: the prefill wins on # load (it consumes and drops the whole hash); the view engine # then reads nothing - the ported parser mirrors that by gating. check(filters_from_hash("#new?project=7") is None, "a load-time prefill never double-applies as a view") # ------------------------------------------------------------------ print("== bet 5: the work stream (joined, ordered, capped, linked) ==") # Seed the three row grammars through the ledger the timeline # reads: a task progress line, a goal-scoped narrative event and # a plain task comment (log highlight). conn = db() conn.execute( "insert into tasks (project_id, title, status, priority, created_at)" " values (1, 'T10 stream task', 'working', 5, %s)" % now) stream_task = conn.execute( "select last_insert_rowid()").fetchone()[0] conn.execute( "insert into goals (project_id, title, brief, branch_name, status," " created_at) values (1, 'T10 stream goal', 'The stream probe.'," " 'goal/t10', 'running', %s)" % now) stream_goal = conn.execute( "select last_insert_rowid()").fetchone()[0] # Goal runs are internal - their task rows never stream. conn.execute( "insert into tasks (project_id, goal_id, kind, title, status," " priority, created_at) values (1, ?, 'goal_run'," " 'T10 internal run', 'working', 5, %s)" % now, (stream_goal,)) run_task = conn.execute( "select last_insert_rowid()").fetchone()[0] def event(kind, scope_type, scope_id, payload, offset): conn.execute( "insert into events (kind, actor, scope_type, scope_id," " payload, created_at) values (?, 'model', ?, ?, ?," " strftime('%s','now') - ?)", (kind, scope_type, scope_id, payload, offset)) event("progress", "task", stream_task, '{"text": "T10 progress line"}', 30) event("state_change", "goal", stream_goal, '{"to": "running", "text": "T10 goal narrative"}', 20) event("comment", "task", stream_task, '{"author": "human", "body": "T10 comment highlight"}', 10) event("progress", "task", run_task, '{"text": "T10 internal run progress"}', 5) conn.commit() conn.close() dash = slot_public(auth, "/", "dashboard") stream = dash.get("work_stream", []) check(isinstance(stream, list), "the dashboard slot carries the work_stream rows") check(0 < len(stream) <= 15, "the stream is capped at 15 rows (got %d)" % len(stream)) epochs = [int(row["at_epoch"]) for row in stream] check(epochs == sorted(epochs, reverse=True), "the stream is newest-first") progress_rows = [row for row in stream if row["kind"] == "task_progress"] goal_rows = [row for row in stream if row["kind"] == "goal_event"] highlight_rows = [row for row in stream if row["kind"] == "log_highlight"] check(any("T10 progress line" in row["text"] and int(row["task_id"]) == stream_task and row["href"] == "/tasks/%d" % stream_task for row in progress_rows), "task progress rows join with the task link") check(any("T10 goal narrative" in row["text"] and int(row["goal_id"]) == stream_goal and row["href"] == "/goals/%d" % stream_goal for row in goal_rows), "goal narrative rows join with the goal link") check(any("T10 comment highlight" in row["text"] and int(row["task_id"]) == stream_task for row in highlight_rows), "comment rows stream as log highlights") check(all(not ("internal run" in row["text"] or int(row.get("task_id", 0)) == run_task) for row in stream), "goal-run internals never stream (the goal carries them)") # The three grammars agree with the activity projection they # derive from (one window, two projections - same order). activity = dash.get("activity", []) check(len(stream) <= len(activity), "the stream is the window's newest slice (activity keeps the full window)") check(len(activity) > 15 or len(stream) == len(activity), "under the cap the stream covers the whole window") # Every row links somewhere or is a bare highlight. for row in stream: if row["href"]: check(row["href"].startswith(("/tasks/", "/goals/")), "row hrefs stay in-app (%s)" % row["href"]) break print("== bet 5: the dashboard renders the stream as its feed ==") home_page = http_get_page(auth, "/") check('data-work-stream' in home_page, "the dashboard ships the work-stream feed") check("work-stream" in home_page and "dashboard.work_stream" in home_page, "the feed renders the slot's work_stream rows") check("Work stream" in home_page, "the feed is titled as the work stream") check("badge-stream-" in home_page, "the rows carry the stream's badge vocabulary") # The old three-grammar surfaces it replaces on THIS page: the # activity table is gone from the dashboard. check("dash-activity-table" not in home_page, "the old activity table is gone from the dashboard") check("data-activity-toggle" not in home_page, "the activity Show-all toggle is gone (the server caps)") check("Show all" not in home_page, "no stale Show-all control remains") # The new-since badge keeps working off the stream rows. check("state.work_stream" in home_page, "the new-since badge counts stream rows") # The underlying surfaces live elsewhere (not deleted): the goal # page narrative + the task log stay their pages' own. check("data-activity-new" in home_page, "the ambient expander frame (with the new badge) keeps the feed quiet") print("== bet 5: the stream survives the pulse refresh ==") refresh = dash.get("refresh") check(refresh is not None, "the dashboard refresh action is authored") act(auth, refresh, {}) dash2 = slot_public(auth, "/", "dashboard") check(len(dash2.get("work_stream", [])) > 0, "the sealed refresh rebuilds the stream too")