|
|
@@ -96,6 +96,15 @@ Plan 11 task D+E (UI + notifications), the `push` shard:
|
|
|
decryption + ES256 JWT signature) because the harness owns the
|
|
|
browser-side keys
|
|
|
|
|
|
+Heredoc completion reports (pages shard):
|
|
|
+
|
|
|
+- terminal tasks render tasks.summary (set by done/fail) as a
|
|
|
+ markdown completion-report card below the status/description grid —
|
|
|
+ "Completion report" when done, "Failure summary" when failed, no
|
|
|
+ card when empty; the summary renders through the shipped
|
|
|
+ stm-markdown renderer (headings/code, no syntax leaks, HTML
|
|
|
+ escaped) and the dashboard timeline snippets its first line
|
|
|
+
|
|
|
Speed pass (plan 11, task 0):
|
|
|
|
|
|
- SHARDING: the scenario groups are partitioned into seven independent
|
|
|
@@ -1556,6 +1565,133 @@ def bundle_pages(ctx):
|
|
|
check(history_page.index("Show history") < history_page.index("edit-expander"),
|
|
|
"the edit expander sits under the Show history expander")
|
|
|
|
|
|
+ print("== heredoc completion reports: markdown card + timeline snippet ==")
|
|
|
+ # The daemon's `cat << 'EOF' | saddle done` stores a markdown
|
|
|
+ # summary (the failure path stores the same field); the task page
|
|
|
+ # renders it as a full-width completion-report card directly below
|
|
|
+ # the status/description grid — markdown via stm-markdown
|
|
|
+ # (XSS-safe), "Completion report" when done, "Failure summary"
|
|
|
+ # when failed, no card when the summary is empty. The dashboard
|
|
|
+ # timeline snippets the summary's FIRST line.
|
|
|
+ report_md = ("## Flange wired and tested\n\n"
|
|
|
+ "Ran `meson test` over the full matrix.\n\n"
|
|
|
+ "```sh\nmeson test -C builddir\n```\n\n"
|
|
|
+ "<b>not html</b> & escaped\n")
|
|
|
+ conn = db()
|
|
|
+ conn.execute("insert into tasks (project_id, title, status, priority,"
|
|
|
+ " summary, started_at, completed_at, created_at)"
|
|
|
+ " values (1, 'Report done task', 'done', 5, ?,"
|
|
|
+ " strftime('%s','now'), strftime('%s','now'),"
|
|
|
+ " strftime('%s','now'))", (report_md,))
|
|
|
+ report_done = conn.execute("select max(id) from tasks").fetchone()[0]
|
|
|
+ conn.execute("insert into tasks (project_id, title, status, priority,"
|
|
|
+ " summary, started_at, completed_at, created_at)"
|
|
|
+ " values (1, 'Report failed task', 'failed', 5,"
|
|
|
+ " 'Boom: tests failed\n\nsecond line carries the detail',"
|
|
|
+ " strftime('%s','now'), strftime('%s','now'),"
|
|
|
+ " strftime('%s','now'))")
|
|
|
+ report_failed = conn.execute("select max(id) from tasks").fetchone()[0]
|
|
|
+ conn.execute("insert into tasks (project_id, title, status, priority,"
|
|
|
+ " started_at, completed_at, created_at)"
|
|
|
+ " values (1, 'Report empty task', 'done', 5,"
|
|
|
+ " strftime('%s','now'), strftime('%s','now'),"
|
|
|
+ " strftime('%s','now'))")
|
|
|
+ report_empty = conn.execute("select max(id) from tasks").fetchone()[0]
|
|
|
+ conn.commit()
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+ done_state = task_state(auth, report_done)
|
|
|
+ check(done_state["report_title"] == "Completion report"
|
|
|
+ and "## Flange wired" in (done_state.get("summary") or ""),
|
|
|
+ "a done task's summary rides the report card state")
|
|
|
+ check(task_state(auth, report_failed)["report_title"] == "Failure summary",
|
|
|
+ "a failed task's card heading reads Failure summary")
|
|
|
+ empty_state = task_state(auth, report_empty)
|
|
|
+ check(empty_state["report_title"] == "" and not (empty_state.get("summary") or ""),
|
|
|
+ "an empty summary renders no report card (no heading)")
|
|
|
+ check(task_state(auth, layout_task)["report_title"] == "",
|
|
|
+ "non-terminal tasks never show the report card")
|
|
|
+
|
|
|
+ status, page = http("GET", "/tasks/%d" % report_done, timeout=15)
|
|
|
+ check(status == 200 and "completion-report" in page
|
|
|
+ and page.index("grid-2") < page.index("completion-report")
|
|
|
+ < page.index("edit-expander"),
|
|
|
+ "the completion-report card sits below the status/description grid, full width")
|
|
|
+ card = page[page.index("completion-report"):page.index("edit-expander")]
|
|
|
+ check('stm-markdown="task.summary"' in card
|
|
|
+ and 'stm-text="task.report_title"' in card,
|
|
|
+ "the card renders the summary as markdown under its state-driven heading")
|
|
|
+ check("<strong>Summary:</strong>" not in page,
|
|
|
+ "the old plain-text summary line is gone (the card owns the summary)")
|
|
|
+
|
|
|
+ # The markdown itself renders client-side: drive the SHIPPED
|
|
|
+ # stm-markdown renderer (statum.js) on the fixture summary via
|
|
|
+ # node — headings and code come out as HTML, no markdown syntax
|
|
|
+ # leaks, raw HTML stays escaped.
|
|
|
+ status, statum_js = http("GET", "/_statum/resource/statum.js", timeout=15)
|
|
|
+ skip = None if status == 200 else "statum.js not served"
|
|
|
+ parts = []
|
|
|
+ if skip is None:
|
|
|
+ def extract_md(name, source):
|
|
|
+ start = source.index("function %s(" % name)
|
|
|
+ depth = 0
|
|
|
+ index = source.index("{", start)
|
|
|
+ end = index
|
|
|
+ while True:
|
|
|
+ if source[end] == "{":
|
|
|
+ depth += 1
|
|
|
+ elif source[end] == "}":
|
|
|
+ depth -= 1
|
|
|
+ if depth == 0:
|
|
|
+ break
|
|
|
+ end += 1
|
|
|
+ return source[start:end + 1]
|
|
|
+ try:
|
|
|
+ parts = [extract_md(name, statum_js)
|
|
|
+ for name in ("safeMarkdownUrl", "mdLinks", "mdInline", "renderMarkdown")]
|
|
|
+ except ValueError:
|
|
|
+ skip = "renderer functions missing"
|
|
|
+ rendered = ""
|
|
|
+ if skip is None:
|
|
|
+ try:
|
|
|
+ result = subprocess.run(
|
|
|
+ ["node", "-e", "%s\n%s\n%s\n%s\nconsole.log(renderMarkdown(process.argv[1]));"
|
|
|
+ % tuple(parts), report_md],
|
|
|
+ capture_output=True, text=True, timeout=20)
|
|
|
+ rendered = result.stdout
|
|
|
+ except (OSError, subprocess.SubprocessError):
|
|
|
+ skip = "node unavailable"
|
|
|
+ if skip is not None:
|
|
|
+ check(True, "stm-markdown renderer present (%s, deep check skipped)" % skip)
|
|
|
+ else:
|
|
|
+ check("<h2>Flange wired and tested</h2>" in rendered
|
|
|
+ and "<pre><code" in rendered and "<code>meson test</code>" in rendered,
|
|
|
+ "the report summary renders headings and code through stm-markdown")
|
|
|
+ check("## Flange" not in rendered and "```sh" not in rendered
|
|
|
+ and "`meson test`" not in rendered,
|
|
|
+ "no raw markdown syntax leaks into the rendered report")
|
|
|
+ check("<b>not html</b>" not in rendered and "<b>not html</b>" in rendered
|
|
|
+ and "& escaped" in rendered,
|
|
|
+ "raw HTML in the summary stays escaped (XSS-safe)")
|
|
|
+
|
|
|
+ # The dashboard timeline: completion/failure events snippet the
|
|
|
+ # summary's first line, falling back to the bare verdict text.
|
|
|
+ dash = slot_public(auth, "/", "dashboard")
|
|
|
+ done_event = next((e for e in dash.get("activity", [])
|
|
|
+ if e["kind"] == "completed" and int(e["task_id"]) == report_done), None)
|
|
|
+ check(done_event is not None
|
|
|
+ and done_event["text"] == "Task completed: ## Flange wired and tested",
|
|
|
+ "the timeline completion event snippets the summary's first line")
|
|
|
+ failed_event = next((e for e in dash.get("activity", [])
|
|
|
+ if e["kind"] == "failed" and int(e["task_id"]) == report_failed), None)
|
|
|
+ check(failed_event is not None
|
|
|
+ and failed_event["text"] == "Task failed: Boom: tests failed",
|
|
|
+ "the timeline failure event snippets the summary's first line")
|
|
|
+ empty_event = next((e for e in dash.get("activity", [])
|
|
|
+ if e["kind"] == "completed" and int(e["task_id"]) == report_empty), None)
|
|
|
+ check(empty_event is not None and empty_event["text"] == "Task completed",
|
|
|
+ "a missing summary falls back to the bare timeline text")
|
|
|
+
|
|
|
print("== plan 11 task D: block split + queue-screen layout ==")
|
|
|
# One open and one answered question: the open block renders in
|
|
|
# full, the answered one folds behind the history expander.
|