notes.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. """The notes bundle (goal 8 task 3): the dashboard scratchpad — a
  2. personal todo list for batches of changes that are not ready to be
  3. full-on goals yet, with promote-to-goal (a DRAFT goal whose brief is
  4. the whole note body, the note frozen and linked to the goal)."""
  5. from harness import * # noqa: F401,F403 — the shared wire helpers
  6. def run(ctx):
  7. auth = ctx.auth
  8. def dashboard():
  9. return slot_public(auth, "/", "dashboard")
  10. def note_rows():
  11. return dashboard()["notes"]
  12. def note_by_body(fragment):
  13. rows = [n for n in note_rows() if fragment in n["body"]]
  14. return rows[0] if rows else None
  15. def db_one(sql, params=()):
  16. conn = db()
  17. try:
  18. return conn.execute(sql, params).fetchone()
  19. finally:
  20. conn.close()
  21. print("== notes: the dashboard carries the scratchpad ==")
  22. dash = dashboard()
  23. check(dash.get("add_note") is not None,
  24. "the dashboard exposes the add-note action")
  25. check(dash["notes"] == [],
  26. "the scratchpad starts empty")
  27. check(len(dash["projects"]) >= 1,
  28. "the promote form's project options list the projects")
  29. check(any(p["name"] == "demo" for p in dash["projects"]),
  30. "the demo project is a promote option")
  31. print("== notes: add ==")
  32. result = act(auth, dash["add_note"], {"body": ""})
  33. check(any(d.get("type") == "notify" and d.get("kind") == "error" for d in result),
  34. "an empty note body refuses")
  35. act(auth, dash["add_note"],
  36. {"body": "Fix the login redirect\n\nAlso cover the remember-me path in tests."})
  37. act(auth, dashboard()["add_note"], {"body": "Second note, older work"})
  38. rows = note_rows()
  39. check(len(rows) == 2,
  40. "added notes land in the list")
  41. check(rows[0]["body"] == "Second note, older work"
  42. and rows[1]["body"].startswith("Fix the login redirect"),
  43. "notes list newest first")
  44. first = note_by_body("Fix the login redirect")
  45. check(first["done"] is False and str(first["goal_id"]) == "0",
  46. "a fresh note is undone and unpromoted")
  47. check(first.get("toggle") is not None and first.get("delete") is not None
  48. and first.get("promote") is not None,
  49. "an undone unpromoted note carries toggle, delete and promote actions")
  50. print("== notes: toggle done -> ordering flips ==")
  51. act(auth, first["toggle"], {})
  52. rows = note_rows()
  53. check(rows[0]["body"] == "Second note, older work"
  54. and rows[1]["body"].startswith("Fix the login redirect"),
  55. "a done note sinks below every undone note")
  56. done_row = note_by_body("Fix the login redirect")
  57. check(done_row["done"] is True and len(done_row.get("done_display") or "") > 0,
  58. "the done flag sticks with its stamp")
  59. check(done_row.get("promote") is None,
  60. "a done note offers no promote action")
  61. conn = db()
  62. stamped = conn.execute(
  63. "select done, done_at from notes where id = ?", (first["id"],)).fetchone()
  64. conn.close()
  65. check(stamped is not None and stamped[0] == 1 and stamped[1] is not None,
  66. "the done transition stamped done_at in the database")
  67. # Toggle back: the note returns to the top and promote returns.
  68. act(auth, done_row["toggle"], {})
  69. rows = note_rows()
  70. check(rows[0]["body"] == "Second note, older work"
  71. and rows[1]["body"].startswith("Fix the login redirect"),
  72. "untoggling restores the newest-first ordering among undone notes")
  73. reopened = note_by_body("Fix the login redirect")
  74. check(reopened["done"] is False and reopened.get("promote") is not None,
  75. "an undone note regains its promote action")
  76. conn = db()
  77. cleared = conn.execute(
  78. "select done, done_at from notes where id = ?", (first["id"],)).fetchone()
  79. conn.close()
  80. check(cleared[0] == 0 and cleared[1] is None,
  81. "untoggling clears the done stamp")
  82. print("== notes: delete ==")
  83. act(auth, dashboard()["add_note"], {"body": "Throwaway note"})
  84. doomed = note_by_body("Throwaway note")
  85. act(auth, doomed["delete"], {})
  86. check(note_by_body("Throwaway note") is None,
  87. "a deleted note disappears from the list")
  88. conn = db()
  89. leftover = conn.execute(
  90. "select count(*) from notes where body like 'Throwaway%'").fetchone()[0]
  91. conn.close()
  92. check(leftover == 0, "the deleted note's row is gone")
  93. print("== notes: promote to goal ==")
  94. body = ("Fix the login redirect\n\nAlso cover the remember-me path in tests.\n"
  95. "The redirect should honour the originally requested page.")
  96. act(auth, dashboard()["add_note"], {"body": body})
  97. note = note_by_body("Fix the login redirect")
  98. promote = note["promote"]
  99. result = act(auth, promote,
  100. {"project_id": "1", "branch_name": "goal/note-promote"})
  101. navigated = [d for d in result if d.get("type") == "navigate"]
  102. check(len(navigated) == 1 and navigated[0].get("uri", "").startswith("/goals/"),
  103. "promoting navigates to the created goal's page")
  104. goal_id = int(navigated[0]["uri"].rsplit("/", 1)[1])
  105. goal_row = db_one("select id, project_id, title, brief, branch_name, status"
  106. " from goals where id = ?", (goal_id,))
  107. check(goal_row is not None,
  108. "promoting created a goal row")
  109. check(goal_row[1] == 1 and goal_row[4] == "goal/note-promote"
  110. and goal_row[5] == "draft",
  111. "the promoted goal sits on the chosen project and branch in draft")
  112. check(goal_row[3] == body,
  113. "the promoted goal's brief is the full note body")
  114. check(goal_row[2] == "Fix the login redirect",
  115. "the promoted goal's title is the note's first line")
  116. state = goal_state(goal_id)
  117. check(state.get("exists") and state.get("title") == "Fix the login redirect",
  118. "the goal page shows the promoted goal")
  119. rows = note_rows()
  120. promoted = [n for n in rows if n["id"] == note["id"]][0]
  121. check(str(promoted["goal_id"]) == str(goal_id)
  122. and promoted["goal_title"] == "Fix the login redirect",
  123. "the promoted note links its goal with the goal's title")
  124. check(promoted.get("promote") is None,
  125. "a promoted note is frozen (no further promote)")
  126. # Promoting a promoted note refuses — the action DTO is gone from
  127. # the slot, so the service-level guard is exercised via a direct
  128. # re-promote attempt through the old DTO.
  129. again = act(auth, promote, {"project_id": "1", "branch_name": "goal/note-promote-2"})
  130. check(any(d.get("type") == "notify" and d.get("kind") == "error"
  131. and "already promoted" in (d.get("message") or "") for d in again),
  132. "re-promoting a promoted note refuses with the frozen error")
  133. conn = db()
  134. duplicates = conn.execute(
  135. "select count(*) from goals where branch_name = 'goal/note-promote-2'"
  136. ).fetchone()[0]
  137. conn.close()
  138. check(duplicates == 0, "the refused re-promotion created no second goal")
  139. print("== notes: promote validation ==")
  140. act(auth, dashboard()["add_note"], {"body": "Never promoted\nbody"})
  141. bad = note_by_body("Never promoted")
  142. missing = act(auth, bad["promote"], {"project_id": "9999",
  143. "branch_name": "goal/notes-bad"})
  144. check(any(d.get("type") == "notify" and d.get("kind") == "error" for d in missing),
  145. "promoting onto a missing project refuses")
  146. nobranch = act(auth, note_by_body("Never promoted")["promote"],
  147. {"project_id": "1", "branch_name": ""})
  148. check(any(d.get("type") == "notify" and d.get("kind") == "error" for d in nobranch),
  149. "promoting without a branch name refuses")
  150. survivor = note_by_body("Never promoted")
  151. check(str(survivor["goal_id"]) == "0" and survivor.get("promote") is not None,
  152. "the refused promotions left the note unpromoted")
  153. print("== notes: the long-title clamp ==")
  154. long_line = "L" * 120 + "\nand a second line"
  155. act(auth, dashboard()["add_note"], {"body": long_line})
  156. clamped = note_by_body("LLLL")
  157. result = act(auth, clamped["promote"],
  158. {"project_id": "1", "branch_name": "goal/note-clamp"})
  159. goal_id2 = int([d for d in result if d.get("type") == "navigate"][0]["uri"]
  160. .rsplit("/", 1)[1])
  161. title = db_one("select title from goals where id = ?", (goal_id2,))[0]
  162. check(len(title) <= 81 and title.endswith("…"),
  163. "a first line over 80 characters clamps with an ellipsis")
  164. brief = db_one("select brief from goals where id = ?", (goal_id2,))[0]
  165. check(brief == long_line,
  166. "the clamped title never truncates the brief (the full body rides along)")