understanding.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. """The understanding bundle (goal 24 task 2): the project Understanding
  2. ledger — the /api/saddle/understanding insert/read callback pair, the
  3. immutable history rows, and the current document riding the assignment
  4. payload."""
  5. from harness import * # noqa: F401,F403 — the shared wire helpers
  6. def run(ctx):
  7. auth, fake, fake_id, fake_token = ctx.auth, ctx.fake, ctx.fake_id, ctx.fake_token
  8. def db_rows(sql, params=()):
  9. conn = db()
  10. try:
  11. return conn.execute(sql, params).fetchall()
  12. finally:
  13. conn.close()
  14. print("== understanding: the migration ==")
  15. # A fresh database: M0001..M0004 ran at startup, so the table
  16. # exists with the goal-24 shape.
  17. tables = [r[0] for r in db_rows(
  18. "select name from sqlite_master where type = 'table'")]
  19. check("project_understandings" in tables,
  20. "M0004 creates the project_understandings table on a fresh DB")
  21. columns = [r[1] for r in db_rows("pragma table_info(project_understandings)")]
  22. check(columns == ["id", "project_id", "body", "commit_hash", "task_id",
  23. "created_at"],
  24. "the table carries the goal-24 columns (%s)" % columns)
  25. print("== understanding: POST auth + validation ==")
  26. status, _ = saddle_api("POST", "/api/saddle/understanding", None,
  27. {"project": "demo", "body": "x"})
  28. check(status == 401, "POST rejects a missing token")
  29. status, _ = saddle_api("POST", "/api/saddle/understanding", "saddle-bogus",
  30. {"project": "demo", "body": "x"})
  31. check(status == 401, "POST rejects a wrong token")
  32. status, body = saddle_api("POST", "/api/saddle/understanding", fake_token,
  33. {"project": "no-such-project", "body": "x"})
  34. check(status == 400 and body.get("ok") is False,
  35. "POST rejects an unknown project")
  36. status, body = saddle_api("POST", "/api/saddle/understanding", fake_token,
  37. {"project": "demo", "body": " "})
  38. check(status == 400 and body.get("ok") is False,
  39. "POST rejects an empty body")
  40. print("== understanding: inserts append history ==")
  41. status, body = saddle_api("POST", "/api/saddle/understanding", fake_token,
  42. {"project": "demo",
  43. "body": "# Demo understanding\n\nFirst revision.",
  44. "commit_hash": "abc1000", "task_id": 11})
  45. check(status == 200 and body.get("ok") is True
  46. and int(body.get("understanding_id", 0)) > 0,
  47. "POST inserts and returns the understanding id")
  48. first_id = int(body["understanding_id"])
  49. status, body = saddle_api("POST", "/api/saddle/understanding", fake_token,
  50. {"project": "demo",
  51. "body": "# Demo understanding\n\nSecond revision.",
  52. "commit_hash": "def2000"})
  53. check(status == 200 and int(body["understanding_id"]) > first_id,
  54. "a refresh appends a LATER history row")
  55. second_id = int(body["understanding_id"])
  56. rows = db_rows("select id, body, commit_hash, task_id"
  57. " from project_understandings order by id")
  58. check([r[0] for r in rows] == [first_id, second_id],
  59. "history rows are immutable appends in insert order")
  60. check(rows[0][2] == "abc1000" and rows[0][3] == 11
  61. and rows[1][2] == "def2000" and rows[1][3] == 0,
  62. "rows keep the commit hash and the optional task id")
  63. print("== understanding: GET reads the current revision ==")
  64. status, _ = saddle_api("GET", "/api/saddle/understanding?project=demo", None)
  65. check(status == 401, "GET rejects a missing token")
  66. status, body = saddle_api(
  67. "GET", "/api/saddle/understanding?project=no-such-project", fake_token)
  68. check(status == 400, "GET rejects an unknown project")
  69. status, body = saddle_api("GET", "/api/saddle/understanding", fake_token)
  70. check(status == 400, "GET requires the project query parameter")
  71. status, body = saddle_api("GET", "/api/saddle/understanding?project=demo",
  72. fake_token)
  73. check(status == 200 and body.get("ok") is True,
  74. "GET answers with the current understanding")
  75. current = body.get("understanding") or {}
  76. check(current.get("body") == "# Demo understanding\n\nSecond revision.",
  77. "GET returns the NEWEST revision (%r)" % current.get("body", "")[:40])
  78. check(current.get("commit_hash") == "def2000"
  79. and (current.get("created") or "").endswith("Z"),
  80. "GET carries the commit hash and an ISO-8601 stamp")
  81. # A project without any understanding answers null, not an error.
  82. state = slot_public(auth, "/projects", "projects")
  83. act(auth, state["create"], {"name": "plain",
  84. "git_url": "https://example.com/plain.git"})
  85. status, body = saddle_api("GET", "/api/saddle/understanding?project=plain",
  86. fake_token)
  87. check(status == 200 and body.get("ok") is True
  88. and body.get("understanding") is None,
  89. "GET answers understanding null for a project with none")
  90. print("== understanding: rides the assignment payload ==")
  91. payload_count = len(fake.start_payloads)
  92. create_queued(auth, {
  93. "project_id": "1",
  94. "title": "Understanding dispatch",
  95. "description": "The current understanding rides the /start payload.",
  96. "priority": "2",
  97. })
  98. check(wait_for(lambda: len(fake.start_payloads) > payload_count or False,
  99. timeout=30, what="understanding /start push") is not None,
  100. "dispatcher pushes the understanding task")
  101. payload = fake.start_payloads[-1]
  102. check("understanding" in payload,
  103. "the assignment payload carries an understanding member")
  104. check(payload.get("understanding") == "# Demo understanding\n\nSecond revision.",
  105. "the payload's understanding is the project's CURRENT revision")
  106. # A giant revision is head-clamped at 16000 chars (the payload cap).
  107. # created_at goes in as a raw epoch INTEGER like every other bundle
  108. # fixture: the ORM materialises DateTime columns from int64 (a text
  109. # value in a DATETIME column fails the element conversion, which
  110. # the project page read surfaces as a 500).
  111. conn = db()
  112. conn.execute("insert into project_understandings"
  113. " (project_id, body, commit_hash, task_id, created_at)"
  114. " values (1, ?, 'big0000', 0, strftime('%s','now'))",
  115. ("giant!" + "y" * 25000,))
  116. conn.commit()
  117. conn.close()
  118. payload_count = len(fake.start_payloads)
  119. create_queued(auth, {
  120. "project_id": "1",
  121. "title": "Understanding cap dispatch",
  122. "priority": "2",
  123. })
  124. check(wait_for(lambda: len(fake.start_payloads) > payload_count or False,
  125. timeout=30, what="cap /start push") is not None,
  126. "dispatcher pushes the cap task")
  127. shipped = fake.start_payloads[-1].get("understanding", "")
  128. check(len(shipped) == 16000 and shipped.startswith("giant!"),
  129. "the payload caps the understanding at 16000 chars, head kept (%d)"
  130. % len(shipped))
  131. # No understanding yet: the member is present but empty.
  132. plain_id = int([p["id"] for p in
  133. slot_public(auth, "/projects", "projects")["projects"]
  134. if p["name"] == "plain"][0])
  135. payload_count = len(fake.start_payloads)
  136. create_queued(auth, {
  137. "project_id": str(plain_id),
  138. "title": "No-understanding dispatch",
  139. "priority": "2",
  140. })
  141. check(wait_for(lambda: len(fake.start_payloads) > payload_count or False,
  142. timeout=30, what="empty /start push") is not None,
  143. "dispatcher pushes the no-understanding task")
  144. check(fake.start_payloads[-1].get("understanding") == "",
  145. "a project without an understanding ships an empty string")
  146. print("== understanding: the project page section ==")
  147. # 'plain' has no understanding: empty current row, empty history,
  148. # but the refresh action is authored.
  149. plain_state = slot_public(auth, "/projects/%d" % plain_id, "project")
  150. check(plain_state.get("refresh_understanding") is not None,
  151. "the project slot authors the refresh action")
  152. check((plain_state.get("understanding_current") or {}).get("body", "x") == "",
  153. "a project without an understanding shows an empty current row")
  154. check(plain_state.get("understanding_history") == [],
  155. "a project without an understanding shows no history rows")
  156. # 'demo' (project 1) has three revisions by now (two API inserts +
  157. # the giant direct row): current is the giant, history is the two
  158. # API revisions newest first.
  159. demo_state = slot_public(auth, "/projects/1", "project")
  160. current_row = demo_state.get("understanding_current") or {}
  161. check(current_row.get("body", "").startswith("giant!"),
  162. "the page's current understanding is the newest revision")
  163. check(current_row.get("commit_hash") == "big0000"
  164. and current_row.get("has_commit") is True,
  165. "the current row carries the commit hash stamp flag")
  166. history = demo_state.get("understanding_history") or []
  167. check([h["commit_hash"] for h in history] == ["def2000", "abc1000"],
  168. "the history lists the older revisions newest first (current not repeated)")
  169. check(history[0]["body"].endswith("Second revision.")
  170. and history[1]["body"].endswith("First revision."),
  171. "history rows keep their markdown bodies")
  172. print("== understanding: Refresh spins up the special task ==")
  173. # The 'plain' project: no notes, no understanding — a first refresh.
  174. result = act(auth, plain_state["refresh_understanding"], {})
  175. check(any(d.get("type") == "notify" and d.get("kind") == "success" for d in result),
  176. "the first refresh notifies success")
  177. rows = db_rows("select id, title, status, mode, kind, description from tasks"
  178. " where title like 'Understanding refresh: plain'")
  179. check(len(rows) == 1, "refresh created exactly one task")
  180. refresh_id = rows[0][0]
  181. check(rows[0][2] == "queued" and rows[0][3] == "investigate"
  182. and rows[0][4] == "task",
  183. "the refresh task is queued, investigate mode, kind task (%s)" % (rows[0][2:5],))
  184. brief = rows[0][5] or ""
  185. check("none — first refresh" in brief,
  186. "a first refresh embeds 'none — first refresh' as the previous hash")
  187. check("Previous document:" not in brief,
  188. "a first refresh carries no previous document")
  189. check("save_understanding" in brief and "git rev-parse HEAD" in brief,
  190. "the brief mandates the save_understanding finishing move")
  191. check("do not edit any files" in brief,
  192. "the brief states the investigate-mode no-edit constraint")
  193. # A system comment marks the refresh on the task thread.
  194. conn = db()
  195. marker = conn.execute(
  196. "select count(*) from task_comments where task_id = ? and author = 'system'"
  197. " and body like 'Understanding refresh queued%'",
  198. (refresh_id,)).fetchone()[0]
  199. conn.close()
  200. check(marker == 1, "the refresh task carries the system comment marker")
  201. print("== understanding: a live refresh refuses a second ==")
  202. plain_state = slot_public(auth, "/projects/%d" % plain_id, "project")
  203. refused = act(auth, plain_state["refresh_understanding"], {})
  204. check(any(d.get("type") == "notify" and d.get("kind") == "error"
  205. and "already running" in (d.get("message") or "") for d in refused),
  206. "a second refresh while the task is live refuses")
  207. rows = db_rows("select count(*) from tasks"
  208. " where title like 'Understanding refresh: plain'")
  209. check(rows[0][0] == 1, "the refused refresh created no second task")
  210. print("== understanding: refresh against an existing document ==")
  211. # The demo project has a current understanding + project notes:
  212. # the brief must embed the old hash, the old document and the notes.
  213. conn = db()
  214. conn.execute("insert into project_notes (project_id, source_task_id, body,"
  215. " created_at) values (1, 0, ?, strftime('%s','now'))",
  216. ("Demo note one: the test harness lives in test/.",))
  217. conn.execute("insert into project_notes (project_id, source_task_id, body,"
  218. " created_at) values (1, 0, ?, strftime('%s','now') + 1)",
  219. ("Demo note two: meson builds everything.",))
  220. conn.commit()
  221. conn.close()
  222. demo_state = slot_public(auth, "/projects/1", "project")
  223. result = act(auth, demo_state["refresh_understanding"], {})
  224. check(any(d.get("type") == "notify" and d.get("kind") == "success" for d in result),
  225. "refreshing a project with an understanding succeeds")
  226. row = db_rows("select id, description from tasks"
  227. " where title like 'Understanding refresh: demo'")[0]
  228. demo_brief = row[1]
  229. check("Previous commit hash: big0000" in demo_brief,
  230. "the brief embeds the previous document's commit hash")
  231. check("```markdown" in demo_brief and "giant!" in demo_brief,
  232. "the brief embeds the previous document in a markdown fence")
  233. check("git log" in demo_brief and "..HEAD" in demo_brief,
  234. "the brief directs the git-history review since the old hash")
  235. check("Project notes from other agents:" in demo_brief
  236. and "Demo note one: the test harness lives in test/." in demo_brief
  237. and "Demo note two: meson builds everything." in demo_brief,
  238. "the brief embeds every project note")
  239. print("== understanding: the refresh task runs, saves, refresh works again ==")
  240. # The fake saddle picks the queued plain refresh up (it asks when
  241. # idle), runs it to done, and the understanding lands via the API.
  242. check(wait_for(lambda: any(
  243. int(p.get("task", {}).get("id") or 0) == refresh_id
  244. for p in fake.start_payloads) or False,
  245. timeout=30, what="refresh task assignment") is not None,
  246. "the dispatcher assigns the refresh task to the saddle")
  247. fake.push_results("done", summary="explored and saved the understanding")
  248. check(wait_task_done(auth, refresh_id, timeout=30) is not None,
  249. "the refresh task completes")
  250. status, body = saddle_api("POST", "/api/saddle/understanding", fake_token,
  251. {"project": "plain",
  252. "body": "# Plain understanding\n\nFirst map.",
  253. "commit_hash": "fff3000",
  254. "task_id": refresh_id})
  255. check(status == 200 and body.get("ok") is True,
  256. "the completed refresh saves its understanding via the API")
  257. plain_state = slot_public(auth, "/projects/%d" % plain_id, "project")
  258. current_row = plain_state.get("understanding_current") or {}
  259. check(current_row.get("body") == "# Plain understanding\n\nFirst map."
  260. and current_row.get("commit_hash") == "fff3000",
  261. "the page shows the new current understanding after the save")
  262. check(plain_state.get("understanding_history") == [],
  263. "the saved revision is current, not duplicated into history")
  264. # Terminal refresh task: a new refresh is allowed again.
  265. result = act(auth, plain_state["refresh_understanding"], {})
  266. check(any(d.get("type") == "notify" and d.get("kind") == "success" for d in result),
  267. "after the refresh task completes a new refresh is allowed")
  268. rows = db_rows("select count(*) from tasks"
  269. " where title like 'Understanding refresh: plain'")
  270. check(rows[0][0] == 2, "the second refresh created its task")
  271. # The new live task blocks again, and the new current doc is now
  272. # the previous one in the fresh brief.
  273. plain_state = slot_public(auth, "/projects/%d" % plain_id, "project")
  274. refused = act(auth, plain_state["refresh_understanding"], {})
  275. check(any(d.get("type") == "notify" and d.get("kind") == "error"
  276. and "already running" in (d.get("message") or "") for d in refused),
  277. "the new live refresh blocks a third")
  278. row = db_rows("select description from tasks"
  279. " where title like 'Understanding refresh: plain'"
  280. " order by id desc")[0]
  281. check("Previous commit hash: fff3000" in (row[0] or "")
  282. and "First map." in (row[0] or ""),
  283. "the refreshed brief embeds the new previous hash and document")