| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281 |
- """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")
|