discoveries.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. """The discoveries bundle (goal 24): the fleet's global, EDITABLE
  2. knowledge base of non-project facts — the /api/saddle/discovery
  3. insert + update callbacks, the /api/saddle/discoveries search, the
  4. human edit/delete actions on the Discoveries page, and the newest
  5. rows riding the assignment payload."""
  6. from harness import * # noqa: F401,F403 — the shared wire helpers
  7. def run(ctx):
  8. auth, fake, fake_id, fake_token = ctx.auth, ctx.fake, ctx.fake_id, ctx.fake_token
  9. def db_rows(sql, params=()):
  10. conn = db()
  11. try:
  12. return conn.execute(sql, params).fetchall()
  13. finally:
  14. conn.close()
  15. print("== discoveries: the migration ==")
  16. # A fresh database: M0001..M0005 ran at startup, so the table
  17. # exists with the goal-24 shape.
  18. tables = [r[0] for r in db_rows(
  19. "select name from sqlite_master where type = 'table'")]
  20. check("discoveries" in tables,
  21. "M0005 creates the discoveries table on a fresh DB")
  22. columns = [r[1] for r in db_rows("pragma table_info(discoveries)")]
  23. check(columns == ["id", "body", "source_task_id", "created_at",
  24. "updated_at"],
  25. "the table carries the goal-24 columns (%s)" % columns)
  26. print("== discoveries: POST auth + validation ==")
  27. status, _ = saddle_api("POST", "/api/saddle/discovery", None, {"body": "x"})
  28. check(status == 401, "POST rejects a missing token")
  29. status, _ = saddle_api("POST", "/api/saddle/discovery", "saddle-bogus",
  30. {"body": "x"})
  31. check(status == 401, "POST rejects a wrong token")
  32. status, body = saddle_api("POST", "/api/saddle/discovery", fake_token,
  33. {"body": " "})
  34. check(status == 400 and body.get("ok") is False,
  35. "POST rejects an empty body")
  36. print("== discoveries: inserts and ordering ==")
  37. status, body = saddle_api("POST", "/api/saddle/discovery", fake_token,
  38. {"task_id": 21,
  39. "body": "valac chokes on `size` for Invercargill's Vector; use `length`."})
  40. check(status == 200 and body.get("ok") is True
  41. and int(body.get("discovery_id", 0)) > 0,
  42. "POST inserts and returns the discovery id")
  43. first_id = int(body["discovery_id"])
  44. status, body = saddle_api("POST", "/api/saddle/discovery", fake_token,
  45. {"body": "meson setup needs a fresh build dir after editing meson.build custom_targets."})
  46. check(status == 200 and int(body.get("discovery_id", 0)) > first_id,
  47. "a second insert gets a LATER id")
  48. second_id = int(body["discovery_id"])
  49. rows = db_rows("select id, body, source_task_id, created_at, updated_at"
  50. " from discoveries order by id")
  51. check([r[0] for r in rows] == [first_id, second_id],
  52. "rows land in insert order")
  53. check(rows[0][2] == 21 and rows[1][2] == 0,
  54. "rows keep the optional source task (21 / 0)")
  55. check(all(r[3] is not None and r[4] is not None for r in rows),
  56. "created_at and updated_at are stamped")
  57. print("== discoveries: GET search ==")
  58. status, _ = saddle_api("GET", "/api/saddle/discoveries?q=valac", None)
  59. check(status == 401, "GET rejects a missing token")
  60. status, body = saddle_api("GET", "/api/saddle/discoveries?q=VALAC",
  61. fake_token)
  62. check(status == 200 and body.get("ok") is True,
  63. "GET answers a search")
  64. hits = body.get("discoveries") or []
  65. check(len(hits) == 1 and hits[0]["id"] == first_id,
  66. "the search is a case-insensitive substring match over the body")
  67. check((hits[0].get("created") or "").endswith("Z")
  68. and (hits[0].get("updated") or "").endswith("Z"),
  69. "rows carry ISO-8601 created/updated stamps")
  70. status, body = saddle_api("GET", "/api/saddle/discoveries", fake_token)
  71. check(status == 200 and len(body.get("discoveries") or []) == 2
  72. and body["discoveries"][0]["id"] == second_id,
  73. "an empty q lists the newest 20 (newest first)")
  74. print("== discoveries: update refines in place ==")
  75. status, _ = saddle_api("POST", "/api/saddle/discovery/update", None,
  76. {"id": first_id, "body": "x"})
  77. check(status == 401, "update rejects a missing token")
  78. status, body = saddle_api("POST", "/api/saddle/discovery/update",
  79. fake_token, {"id": 999999, "body": "x"})
  80. check(status == 400 and body.get("ok") is False,
  81. "update rejects an unknown id with an error JSON")
  82. status, body = saddle_api("POST", "/api/saddle/discovery/update",
  83. fake_token,
  84. {"id": first_id,
  85. "body": "valac chokes on `size` for Invercargill's Vector; use `length` (also `count`, a uint)."})
  86. check(status == 200 and body.get("ok") is True, "update answers ok")
  87. rows = db_rows("select id, body, source_task_id, created_at, updated_at"
  88. " from discoveries order by id")
  89. check(len(rows) == 2 and rows[0][0] == first_id,
  90. "update INSERTS NOTHING — the row is refined in place")
  91. check("also `count`" in rows[0][1], "the body was replaced")
  92. check(rows[0][2] == 21, "update keeps the original source task")
  93. check(rows[0][4] >= rows[0][3],
  94. "updated_at bumped at or after created_at")
  95. print("== discoveries: the page and the human actions ==")
  96. state = slot_public(auth, "/discoveries", "discoveries")
  97. listed = state.get("discoveries") or []
  98. check([r["id"] for r in listed] == [str(second_id), str(first_id)],
  99. "the page lists rows newest first")
  100. check("also `count`" in listed[1]["body"],
  101. "the page shows the refined body")
  102. check(listed[1].get("updated_display", "") != ""
  103. and listed[0].get("updated_display", "") != "",
  104. "refined rows show an updated stamp")
  105. check(all(r.get("update") is not None and r.get("delete") is not None
  106. for r in listed),
  107. "every row authors the edit and delete actions")
  108. # The human edit path: the same update-in-place semantics.
  109. result = act(auth, listed[1]["update"],
  110. {"body": "valac: Invercargill Vector exposes `length` (uint) and `count()`, never `size`."})
  111. check(any(d.get("type") == "notify" and d.get("kind") == "success"
  112. for d in result), "the human edit notifies success")
  113. rows = db_rows("select body from discoveries where id = %d" % first_id)
  114. check("never `size`" in rows[0][0],
  115. "the human edit replaced the body in place")
  116. # The human delete path.
  117. result = act(auth, listed[0]["delete"], {})
  118. check(any(d.get("type") == "notify" and d.get("kind") == "success"
  119. for d in result), "the human delete notifies success")
  120. rows = db_rows("select id from discoveries order by id")
  121. check([r[0] for r in rows] == [first_id],
  122. "the human delete removed the row")
  123. print("== discoveries: the page search filter ==")
  124. # The browser filters client-side (the script above), but the
  125. # entrypoint still honours a server-side q: a direct entrypoint
  126. # hit passes the filter as a sibling query param next to uri=
  127. # (the same shape the task page's status filter documents).
  128. status, body = http(
  129. "GET",
  130. "/_statum/entrypoint?uri=" + urllib.parse.quote("/discoveries")
  131. + "&q=" + urllib.parse.quote("never `size`"),
  132. headers={"X-Statum-Slot": "key=%s; as_at=%s" % (auth["key"], auth["as_at"])})
  133. check(status == 200, "a direct entrypoint hit with q= answers")
  134. filtered = None
  135. for directive in json.loads(body):
  136. if directive.get("type") == "set":
  137. content = json.loads(directive["snapshot_frame"]["content"])
  138. if content["slot"]["type"] == "discoveries":
  139. filtered = content["public"]
  140. check(filtered is not None and filtered.get("query") == "never `size`",
  141. "the entrypoint seeds the search box from q")
  142. check(len(filtered.get("discoveries") or []) == 1
  143. and "never `size`" in (filtered["discoveries"][0].get("body", "")),
  144. "the q filter narrows the rows server-side")
  145. state = slot_public(auth, "/discoveries", "discoveries")
  146. check(state.get("query", "") == "" and len(state.get("discoveries") or []) == 1,
  147. "a plain hit stays unfiltered")
  148. print("== discoveries: ride the assignment payload ==")
  149. payload_count = len(fake.start_payloads)
  150. create_queued(auth, {
  151. "project_id": "1",
  152. "title": "Discovery dispatch",
  153. "priority": "2",
  154. })
  155. check(wait_for(lambda: len(fake.start_payloads) > payload_count or False,
  156. timeout=30, what="discovery /start push") is not None,
  157. "dispatcher pushes the discovery task")
  158. payload = fake.start_payloads[-1]
  159. check("discoveries" in payload,
  160. "the assignment payload carries a discoveries member")
  161. shipped = payload.get("discoveries")
  162. check(isinstance(shipped, list) and len(shipped) == 1
  163. and "never `size`" in shipped[0].get("body", ""),
  164. "the payload ships the newest rows as {body} objects")
  165. # The cap: 12 giant rows → at most 10 entries / 8000 chars.
  166. conn = db()
  167. for i in range(12):
  168. conn.execute(
  169. "insert into discoveries (body, source_task_id, created_at, updated_at)"
  170. " values (?, 0, strftime('%s','now'), strftime('%s','now'))",
  171. ("cap%02d " % i + "z" * 1500,))
  172. conn.commit()
  173. conn.close()
  174. payload_count = len(fake.start_payloads)
  175. create_queued(auth, {
  176. "project_id": "1",
  177. "title": "Discovery cap dispatch",
  178. "priority": "2",
  179. })
  180. check(wait_for(lambda: len(fake.start_payloads) > payload_count or False,
  181. timeout=30, what="cap /start push") is not None,
  182. "dispatcher pushes the cap task")
  183. shipped = fake.start_payloads[-1].get("discoveries")
  184. check(isinstance(shipped, list) and len(shipped) <= 10,
  185. "the payload caps the discoveries at 10 entries (%d)" % len(shipped))
  186. check(sum(len(r.get("body", "")) for r in shipped) <= 8000,
  187. "the payload caps the discoveries at 8000 chars total (%d)"
  188. % sum(len(r.get("body", "")) for r in shipped))
  189. check(shipped[0].get("body", "").startswith("cap11"),
  190. "the newest rows ship first")
  191. print("== discoveries: the dashboard slice ==")
  192. home = slot_public(auth, "/", "dashboard")
  193. latest = home.get("discoveries") or []
  194. check(len(latest) == 5 and latest[0].get("body", "").startswith("cap11"),
  195. "the dashboard shows the five newest discoveries newest first")