|
@@ -0,0 +1,183 @@
|
|
|
|
|
+"""The notes bundle (goal 8 task 3): the dashboard scratchpad — a
|
|
|
|
|
+personal todo list for batches of changes that are not ready to be
|
|
|
|
|
+full-on goals yet, with promote-to-goal (a DRAFT goal whose brief is
|
|
|
|
|
+the whole note body, the note frozen and linked to the goal)."""
|
|
|
|
|
+from harness import * # noqa: F401,F403 — the shared wire helpers
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def run(ctx):
|
|
|
|
|
+ auth = ctx.auth
|
|
|
|
|
+
|
|
|
|
|
+ def dashboard():
|
|
|
|
|
+ return slot_public(auth, "/", "dashboard")
|
|
|
|
|
+
|
|
|
|
|
+ def note_rows():
|
|
|
|
|
+ return dashboard()["notes"]
|
|
|
|
|
+
|
|
|
|
|
+ def note_by_body(fragment):
|
|
|
|
|
+ rows = [n for n in note_rows() if fragment in n["body"]]
|
|
|
|
|
+ return rows[0] if rows else None
|
|
|
|
|
+
|
|
|
|
|
+ def db_one(sql, params=()):
|
|
|
|
|
+ conn = db()
|
|
|
|
|
+ try:
|
|
|
|
|
+ return conn.execute(sql, params).fetchone()
|
|
|
|
|
+ finally:
|
|
|
|
|
+ conn.close()
|
|
|
|
|
+
|
|
|
|
|
+ print("== notes: the dashboard carries the scratchpad ==")
|
|
|
|
|
+ dash = dashboard()
|
|
|
|
|
+ check(dash.get("add_note") is not None,
|
|
|
|
|
+ "the dashboard exposes the add-note action")
|
|
|
|
|
+ check(dash["notes"] == [],
|
|
|
|
|
+ "the scratchpad starts empty")
|
|
|
|
|
+ check(len(dash["projects"]) >= 1,
|
|
|
|
|
+ "the promote form's project options list the projects")
|
|
|
|
|
+ check(any(p["name"] == "demo" for p in dash["projects"]),
|
|
|
|
|
+ "the demo project is a promote option")
|
|
|
|
|
+
|
|
|
|
|
+ print("== notes: add ==")
|
|
|
|
|
+ result = act(auth, dash["add_note"], {"body": ""})
|
|
|
|
|
+ check(any(d.get("type") == "notify" and d.get("kind") == "error" for d in result),
|
|
|
|
|
+ "an empty note body refuses")
|
|
|
|
|
+ act(auth, dash["add_note"],
|
|
|
|
|
+ {"body": "Fix the login redirect\n\nAlso cover the remember-me path in tests."})
|
|
|
|
|
+ act(auth, dashboard()["add_note"], {"body": "Second note, older work"})
|
|
|
|
|
+ rows = note_rows()
|
|
|
|
|
+ check(len(rows) == 2,
|
|
|
|
|
+ "added notes land in the list")
|
|
|
|
|
+ check(rows[0]["body"] == "Second note, older work"
|
|
|
|
|
+ and rows[1]["body"].startswith("Fix the login redirect"),
|
|
|
|
|
+ "notes list newest first")
|
|
|
|
|
+ first = note_by_body("Fix the login redirect")
|
|
|
|
|
+ check(first["done"] is False and str(first["goal_id"]) == "0",
|
|
|
|
|
+ "a fresh note is undone and unpromoted")
|
|
|
|
|
+ check(first.get("toggle") is not None and first.get("delete") is not None
|
|
|
|
|
+ and first.get("promote") is not None,
|
|
|
|
|
+ "an undone unpromoted note carries toggle, delete and promote actions")
|
|
|
|
|
+
|
|
|
|
|
+ print("== notes: toggle done -> ordering flips ==")
|
|
|
|
|
+ act(auth, first["toggle"], {})
|
|
|
|
|
+ rows = note_rows()
|
|
|
|
|
+ check(rows[0]["body"] == "Second note, older work"
|
|
|
|
|
+ and rows[1]["body"].startswith("Fix the login redirect"),
|
|
|
|
|
+ "a done note sinks below every undone note")
|
|
|
|
|
+ done_row = note_by_body("Fix the login redirect")
|
|
|
|
|
+ check(done_row["done"] is True and len(done_row.get("done_display") or "") > 0,
|
|
|
|
|
+ "the done flag sticks with its stamp")
|
|
|
|
|
+ check(done_row.get("promote") is None,
|
|
|
|
|
+ "a done note offers no promote action")
|
|
|
|
|
+ conn = db()
|
|
|
|
|
+ stamped = conn.execute(
|
|
|
|
|
+ "select done, done_at from notes where id = ?", (first["id"],)).fetchone()
|
|
|
|
|
+ conn.close()
|
|
|
|
|
+ check(stamped is not None and stamped[0] == 1 and stamped[1] is not None,
|
|
|
|
|
+ "the done transition stamped done_at in the database")
|
|
|
|
|
+ # Toggle back: the note returns to the top and promote returns.
|
|
|
|
|
+ act(auth, done_row["toggle"], {})
|
|
|
|
|
+ rows = note_rows()
|
|
|
|
|
+ check(rows[0]["body"] == "Second note, older work"
|
|
|
|
|
+ and rows[1]["body"].startswith("Fix the login redirect"),
|
|
|
|
|
+ "untoggling restores the newest-first ordering among undone notes")
|
|
|
|
|
+ reopened = note_by_body("Fix the login redirect")
|
|
|
|
|
+ check(reopened["done"] is False and reopened.get("promote") is not None,
|
|
|
|
|
+ "an undone note regains its promote action")
|
|
|
|
|
+ conn = db()
|
|
|
|
|
+ cleared = conn.execute(
|
|
|
|
|
+ "select done, done_at from notes where id = ?", (first["id"],)).fetchone()
|
|
|
|
|
+ conn.close()
|
|
|
|
|
+ check(cleared[0] == 0 and cleared[1] is None,
|
|
|
|
|
+ "untoggling clears the done stamp")
|
|
|
|
|
+
|
|
|
|
|
+ print("== notes: delete ==")
|
|
|
|
|
+ act(auth, dashboard()["add_note"], {"body": "Throwaway note"})
|
|
|
|
|
+ doomed = note_by_body("Throwaway note")
|
|
|
|
|
+ act(auth, doomed["delete"], {})
|
|
|
|
|
+ check(note_by_body("Throwaway note") is None,
|
|
|
|
|
+ "a deleted note disappears from the list")
|
|
|
|
|
+ conn = db()
|
|
|
|
|
+ leftover = conn.execute(
|
|
|
|
|
+ "select count(*) from notes where body like 'Throwaway%'").fetchone()[0]
|
|
|
|
|
+ conn.close()
|
|
|
|
|
+ check(leftover == 0, "the deleted note's row is gone")
|
|
|
|
|
+
|
|
|
|
|
+ print("== notes: promote to goal ==")
|
|
|
|
|
+ body = ("Fix the login redirect\n\nAlso cover the remember-me path in tests.\n"
|
|
|
|
|
+ "The redirect should honour the originally requested page.")
|
|
|
|
|
+ act(auth, dashboard()["add_note"], {"body": body})
|
|
|
|
|
+ note = note_by_body("Fix the login redirect")
|
|
|
|
|
+ promote = note["promote"]
|
|
|
|
|
+ result = act(auth, promote,
|
|
|
|
|
+ {"project_id": "1", "branch_name": "goal/note-promote"})
|
|
|
|
|
+ navigated = [d for d in result if d.get("type") == "navigate"]
|
|
|
|
|
+ check(len(navigated) == 1 and navigated[0].get("uri", "").startswith("/goals/"),
|
|
|
|
|
+ "promoting navigates to the created goal's page")
|
|
|
|
|
+ goal_id = int(navigated[0]["uri"].rsplit("/", 1)[1])
|
|
|
|
|
+
|
|
|
|
|
+ goal_row = db_one("select id, project_id, title, brief, branch_name, status"
|
|
|
|
|
+ " from goals where id = ?", (goal_id,))
|
|
|
|
|
+ check(goal_row is not None,
|
|
|
|
|
+ "promoting created a goal row")
|
|
|
|
|
+ check(goal_row[1] == 1 and goal_row[4] == "goal/note-promote"
|
|
|
|
|
+ and goal_row[5] == "draft",
|
|
|
|
|
+ "the promoted goal sits on the chosen project and branch in draft")
|
|
|
|
|
+ check(goal_row[3] == body,
|
|
|
|
|
+ "the promoted goal's brief is the full note body")
|
|
|
|
|
+ check(goal_row[2] == "Fix the login redirect",
|
|
|
|
|
+ "the promoted goal's title is the note's first line")
|
|
|
|
|
+
|
|
|
|
|
+ state = goal_state(goal_id)
|
|
|
|
|
+ check(state.get("exists") and state.get("title") == "Fix the login redirect",
|
|
|
|
|
+ "the goal page shows the promoted goal")
|
|
|
|
|
+
|
|
|
|
|
+ rows = note_rows()
|
|
|
|
|
+ promoted = [n for n in rows if n["id"] == note["id"]][0]
|
|
|
|
|
+ check(str(promoted["goal_id"]) == str(goal_id)
|
|
|
|
|
+ and promoted["goal_title"] == "Fix the login redirect",
|
|
|
|
|
+ "the promoted note links its goal with the goal's title")
|
|
|
|
|
+ check(promoted.get("promote") is None,
|
|
|
|
|
+ "a promoted note is frozen (no further promote)")
|
|
|
|
|
+
|
|
|
|
|
+ # Promoting a promoted note refuses — the action DTO is gone from
|
|
|
|
|
+ # the slot, so the service-level guard is exercised via a direct
|
|
|
|
|
+ # re-promote attempt through the old DTO.
|
|
|
|
|
+ again = act(auth, promote, {"project_id": "1", "branch_name": "goal/note-promote-2"})
|
|
|
|
|
+ check(any(d.get("type") == "notify" and d.get("kind") == "error"
|
|
|
|
|
+ and "already promoted" in (d.get("message") or "") for d in again),
|
|
|
|
|
+ "re-promoting a promoted note refuses with the frozen error")
|
|
|
|
|
+ conn = db()
|
|
|
|
|
+ duplicates = conn.execute(
|
|
|
|
|
+ "select count(*) from goals where branch_name = 'goal/note-promote-2'"
|
|
|
|
|
+ ).fetchone()[0]
|
|
|
|
|
+ conn.close()
|
|
|
|
|
+ check(duplicates == 0, "the refused re-promotion created no second goal")
|
|
|
|
|
+
|
|
|
|
|
+ print("== notes: promote validation ==")
|
|
|
|
|
+ act(auth, dashboard()["add_note"], {"body": "Never promoted\nbody"})
|
|
|
|
|
+ bad = note_by_body("Never promoted")
|
|
|
|
|
+ missing = act(auth, bad["promote"], {"project_id": "9999",
|
|
|
|
|
+ "branch_name": "goal/notes-bad"})
|
|
|
|
|
+ check(any(d.get("type") == "notify" and d.get("kind") == "error" for d in missing),
|
|
|
|
|
+ "promoting onto a missing project refuses")
|
|
|
|
|
+ nobranch = act(auth, note_by_body("Never promoted")["promote"],
|
|
|
|
|
+ {"project_id": "1", "branch_name": ""})
|
|
|
|
|
+ check(any(d.get("type") == "notify" and d.get("kind") == "error" for d in nobranch),
|
|
|
|
|
+ "promoting without a branch name refuses")
|
|
|
|
|
+ survivor = note_by_body("Never promoted")
|
|
|
|
|
+ check(str(survivor["goal_id"]) == "0" and survivor.get("promote") is not None,
|
|
|
|
|
+ "the refused promotions left the note unpromoted")
|
|
|
|
|
+
|
|
|
|
|
+ print("== notes: the long-title clamp ==")
|
|
|
|
|
+ long_line = "L" * 120 + "\nand a second line"
|
|
|
|
|
+ act(auth, dashboard()["add_note"], {"body": long_line})
|
|
|
|
|
+ clamped = note_by_body("LLLL")
|
|
|
|
|
+ result = act(auth, clamped["promote"],
|
|
|
|
|
+ {"project_id": "1", "branch_name": "goal/note-clamp"})
|
|
|
|
|
+ goal_id2 = int([d for d in result if d.get("type") == "navigate"][0]["uri"]
|
|
|
|
|
+ .rsplit("/", 1)[1])
|
|
|
|
|
+ title = db_one("select title from goals where id = ?", (goal_id2,))[0]
|
|
|
|
|
+ check(len(title) <= 81 and title.endswith("…"),
|
|
|
|
|
+ "a first line over 80 characters clamps with an ellipsis")
|
|
|
|
|
+ brief = db_one("select brief from goals where id = ?", (goal_id2,))[0]
|
|
|
|
|
+ check(brief == long_line,
|
|
|
|
|
+ "the clamped title never truncates the brief (the full body rides along)")
|