t9library.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. """The T9 library bundle (goal 41 task 9): the project detail, rules,
  2. discoveries and chats pages on the Spry conversion — the project page's
  3. Details/Edit twin cards collapsed into ONE in-place editor
  4. (data-long-form) with the freed structure carrying the Goals reverse
  5. link and a create-task deep-link into the /tasks dialog, the rules
  6. page's Edit expander turned in-place with Delete demoted behind a
  7. ghost button and the new blast-radius attachments list, the discoveries
  8. rows' edit expander turned in-place, and breadcrumbs on the project and
  9. chat detail pages. Markup-level via http_get_page; the new slot members
  10. (goals vector, rule attachments, crumbs vectors) cross-checked against
  11. seeded database rows."""
  12. from harness import * # noqa: F401,F403 — the shared wire helpers
  13. def run(ctx):
  14. auth, fake = ctx.auth, ctx.fake
  15. now = "strftime('%s','now')"
  16. print("== project detail: the in-place Details editor ==")
  17. # A dedicated project so the bundle owns every row it asserts on.
  18. state = slot_public(auth, "/projects", "projects")
  19. act(auth, state["create"], {"name": "t9-library",
  20. "git_url": "https://example.com/t9.git",
  21. "default_branch": "main",
  22. "summary": "the T9 probe project"})
  23. rows = slot_public(auth, "/projects", "projects")["projects"]
  24. project_id = int([p["id"] for p in rows if p["name"] == "t9-library"][0])
  25. detail = slot_public(auth, "/projects/%d" % project_id, "project")
  26. check(detail["update"] is not None,
  27. "the project slot authors the update action")
  28. page = http_get_page(auth, "/projects/%d" % project_id)
  29. check('data-long-form data-field="project"' in page,
  30. "the Details card is one data-long-form card")
  31. check(page.count('data-lf-edit') >= 1 and page.count('data-lf-view') >= 1,
  32. "the card carries the read/edit halves")
  33. check('data-lf-src="project.git_url"' in page
  34. and 'data-lf-src="project.summary"' in page,
  35. "the edit half prefills from the slot's current values")
  36. check("<h3>Edit</h3>" not in page,
  37. "the side-by-side Edit card is gone")
  38. check('stm-action="project.update"' in page,
  39. "the editor form posts the project update action")
  40. print("== project detail: breadcrumbs replace the back anchor ==")
  41. check("<p><a href=\"/projects\">All projects</a></p>" not in page,
  42. "the hand-drawn All-projects anchor is gone")
  43. check('stm-def-crumbs-as="project.crumbs"' in page,
  44. "the breadcrumb partial rides the project page")
  45. crumbs = detail["crumbs"]
  46. check([c["label"] for c in crumbs]
  47. == ["Dashboard", "Projects", "t9-library"]
  48. and crumbs[0]["href"] == "/" and crumbs[1]["href"] == "/projects"
  49. and crumbs[2].get("href") is None,
  50. "the crumbs vector trails Dashboard / Projects / <name> (%s)"
  51. % [c["label"] for c in crumbs])
  52. print("== project detail: the create-task deep-link ==")
  53. check("'#new?project=' + project.id" in page
  54. or "'/tasks#new?project=' + project.id" in page,
  55. "the create affordance deep-links the /tasks dialog prefill")
  56. check("New task in this project" in page,
  57. "the affordance names the project flow")
  58. tasks_page = http_get_page(auth, "/tasks")
  59. check("applyNewTaskPrefill" in tasks_page,
  60. "the tasks page ships the prefill pass")
  61. check("task-create-dialog" in tasks_page
  62. and "openAppDialog" in tasks_page,
  63. "the prefill opens T4's create dialog through the helper")
  64. print("== project detail: the goals reverse link ==")
  65. # Seed one primary goal and one co-developed link; a reference link
  66. # must NOT appear (context, not work happening here).
  67. conn = db()
  68. now = "strftime('%s','now')"
  69. conn.execute(
  70. "insert into goals (project_id, title, brief, branch_name, status,"
  71. " priority, creator, created_at) values (?, 'T9 primary goal',"
  72. " 'built here', 'goal/t9-primary', 'active', 3, 'human', %s)"
  73. % now, (project_id,))
  74. primary_goal = int(conn.execute("select max(id) from goals").fetchone()[0])
  75. conn.execute(
  76. "insert into projects (name, git_url, default_branch, created_at)"
  77. " values ('t9-codev', 'https://example.com/t9c.git', 'main', %s)"
  78. % now)
  79. codev_project = int(conn.execute("select max(id) from projects")
  80. .fetchone()[0])
  81. conn.execute(
  82. "insert into goals (project_id, title, brief, branch_name, status,"
  83. " priority, creator, created_at) values (?, 'T9 co-dev goal',"
  84. " 'built alongside', 'goal/t9-codedev', 'planning', 3, 'human', %s)"
  85. % now, (codev_project,))
  86. codev_goal = int(conn.execute("select max(id) from goals").fetchone()[0])
  87. conn.execute(
  88. "insert into goal_links (goal_id, project_id, relationship)"
  89. " values (?, ?, 'co-developed')", (codev_goal, project_id))
  90. conn.execute(
  91. "insert into goals (project_id, title, brief, branch_name, status,"
  92. " priority, creator, created_at) values (?, 'T9 ref goal',"
  93. " 'read only', 'goal/t9-ref', 'draft', 3, 'human', %s)"
  94. % now, (codev_project,))
  95. ref_goal = int(conn.execute("select max(id) from goals").fetchone()[0])
  96. conn.execute(
  97. "insert into goal_links (goal_id, project_id, relationship)"
  98. " values (?, ?, 'reference')", (ref_goal, project_id))
  99. conn.commit()
  100. conn.close()
  101. detail = slot_public(auth, "/projects/%d" % project_id, "project")
  102. goals = {str(g["id"]): g for g in detail["goals"]}
  103. check(set(goals) == {str(primary_goal), str(codev_goal)},
  104. "the goals vector carries the primary and co-developed goals only"
  105. " (got %s)" % sorted(goals))
  106. check(goals[str(primary_goal)]["relationship"] == "primary"
  107. and goals[str(codev_goal)]["relationship"] == "co-developed",
  108. "each goal row names its relationship")
  109. check(goals[str(primary_goal)]["display"] == "active"
  110. and goals[str(codev_goal)]["display"] == "planning",
  111. "the rows carry the human display status")
  112. page = http_get_page(auth, "/projects/%d" % project_id)
  113. check("Goals in this project" in page
  114. and "'/goals/' + goal.id" in page,
  115. "the page renders the goals reverse-link table")
  116. check("'badge badge-' + goal.status" in page,
  117. "goal rows badge their status like the goals list")
  118. print("== rules: the in-place editor + the demoted delete ==")
  119. rules_state = slot_public(auth, "/rules", "rules")
  120. act(auth, rules_state["create"], {
  121. "name": "T9 blast rule", "body": "The blast radius probe rule."})
  122. conn = db()
  123. rule_id = int(conn.execute(
  124. "select id from rules where name = 'T9 blast rule'").fetchone()[0])
  125. # Attach it to the probe project and to one task.
  126. conn.execute(
  127. "insert into rule_projects (rule_id, project_id, created_at)"
  128. " values (?, ?, %s)" % now, (rule_id, project_id))
  129. conn.execute(
  130. "insert into tasks (project_id, title, status, priority, created_at)"
  131. " values (?, 'T9 blast task', 'queued', 5, %s)" % now, (project_id,))
  132. blast_task = int(conn.execute("select max(id) from tasks").fetchone()[0])
  133. conn.execute(
  134. "insert into task_rules (rule_id, task_id, created_at)"
  135. " values (?, ?, %s)" % now, (rule_id, blast_task))
  136. conn.commit()
  137. conn.close()
  138. rules_state = slot_public(auth, "/rules", "rules")
  139. row = [r for r in rules_state["rules"]
  140. if r["name"] == "T9 blast rule"][0]
  141. check([(a["kind"], a["label"]) for a in row["attachments"]]
  142. == [("project", "t9-library"), ("task", "T9 blast task")],
  143. "the rule row lists its blast radius in attachment order (%s)"
  144. % [(a["kind"], a["label"]) for a in row["attachments"]])
  145. check(all(int(a["id"]) > 0 for a in row["attachments"]),
  146. "each attachment carries the linked row's id")
  147. unattached = [r for r in rules_state["rules"]
  148. if len(r.get("attachments") or []) == 0]
  149. check(len(unattached) >= 1
  150. or len(rules_state["rules"]) == 1,
  151. "rules attached nowhere list no attachments (no crash)")
  152. page = http_get_page(auth, "/rules")
  153. check("<summary><strong>New rule</strong></summary>" not in page
  154. and "rule-create-dialog" in page
  155. and "openAppDialog('rule-create-dialog')" in page,
  156. "the create expander became a dialog behind a button")
  157. check("<summary>Edit</summary>" not in page,
  158. "the per-rule Edit expander is gone")
  159. check('data-long-form data-field="rule-row"' in page
  160. and 'data-lf-toggle' in page,
  161. "each rule edits through the in-place engine")
  162. # The compiled page entity-encodes the body text ("…" -> &hellip;);
  163. # assert on the escaped form and on the ghost class itself.
  164. check("Delete rule&hellip;</button>" in page
  165. and 'class="btn-ghost btn-sm"' in page,
  166. "delete is demoted to a ghost button out of the row's face")
  167. check('stm-text="attachment.label"' in page,
  168. "the blast radius renders as links on the rule card")
  169. print("== discoveries: the in-place editor ==")
  170. page = http_get_page(auth, "/discoveries")
  171. check("<summary>Edit</summary>" not in page,
  172. "the discoveries Edit expander is gone")
  173. check('data-long-form data-field="discovery-row"' in page,
  174. "each discovery edits through the in-place engine")
  175. check('stm-text="discovery.body"' in page
  176. and 'name="body"' in page,
  177. "the edit textarea prefills from the rendered row value")
  178. check('discoveryRowVisible' in page and 'setDiscoveryQuery' in page,
  179. "the client-side search semantics are unchanged")
  180. state = slot_public(auth, "/discoveries", "discoveries")
  181. check("query" in state and "more" in state,
  182. "the slot still seeds the search box and the more count")
  183. print("== chats: breadcrumbs on the detail page ==")
  184. # Seed the chat row directly (the slot builder reads any owned row;
  185. # this bundle does not restart the app with a scripted LLM - the
  186. # chatui bundle owns that surface).
  187. conn = db()
  188. conn.execute(
  189. "insert into advisor_chats (user_id, title, status, created_at,"
  190. " updated_at) values ((select id from spry_users where username ="
  191. " 'admin'), 'T9 crumbs probe', 'idle', %s, %s)" % (now, now))
  192. chat_id = int(conn.execute("select max(id) from advisor_chats")
  193. .fetchone()[0])
  194. conn.commit()
  195. conn.close()
  196. chat_uri = "/chats/%d" % chat_id
  197. chat_state = slot_public(auth, chat_uri, "chat")
  198. check(chat_state["id"] == chat_id, "the probe chat exists (%s)" % chat_uri)
  199. crumbs = chat_state["crumbs"]
  200. check([c["label"] for c in crumbs]
  201. == ["Dashboard", "Chats", "T9 crumbs probe"]
  202. and crumbs[0]["href"] == "/" and crumbs[1]["href"] == "/chats"
  203. and crumbs[2].get("href") is None,
  204. "the chat slot trails Dashboard / Chats / <title> (%s)"
  205. % [c["label"] for c in crumbs])
  206. page = http_get_page(auth, chat_uri)
  207. check('stm-def-crumbs-as="chat.crumbs"' in page,
  208. "the breadcrumb partial rides the chat detail page")
  209. check("chat-thread" in page and "chat-composer" in page,
  210. "the conversation still leads with the composer below")
  211. detail = slot_public(auth, "/chats/%d" % chat_id, "chat")
  212. check(detail["delete"] is not None,
  213. "the seeded chat still authors its delete action")
  214. print("== chats list: the demoted delete ==")
  215. page = http_get_page(auth, "/chats")
  216. check('class="btn-ghost btn-sm" stm-action="chat.delete"' in page,
  217. "the list's delete is a quiet ghost button")
  218. check("alert alert-warning" in page,
  219. "the unconfigured-advisor notice uses the Spry alert")