knowledge_cycle.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. """The knowledge-cycle bundle (goal 24 task 6): ONE end-to-end pass
  2. through the understanding and discoveries cycles, cross-cutting the
  3. surfaces the per-repo bundles could only check in isolation.
  4. Understanding: press Refresh on a project → the created task's OWN
  5. assignment payload (what the daemon actually receives) carries the
  6. brief's embedded old hash / old document / project notes AND the
  7. `understanding` member with the current revision → the saddle POSTs
  8. /api/saddle/understanding → the project page shows the new current
  9. document and a history row → a second POST makes the page show the
  10. newer one.
  11. Discoveries: POST via the API → the /discoveries page lists it and the
  12. dashboard shows it → update-by-id refines it → a fresh assignment
  13. payload carries it.
  14. """
  15. from harness import * # noqa: F401,F403 — the shared wire helpers
  16. def run(ctx):
  17. auth, fake, fake_id, fake_token = ctx.auth, ctx.fake, ctx.fake_id, ctx.fake_token
  18. def db_rows(sql, params=()):
  19. conn = db()
  20. try:
  21. return conn.execute(sql, params).fetchall()
  22. finally:
  23. conn.close()
  24. def payload_for(task_id):
  25. for payload in reversed(fake.start_payloads):
  26. if int((payload.get("task") or {}).get("id") or 0) == int(task_id):
  27. return payload
  28. return None
  29. # A dedicated project so the cycle owns every row it asserts on.
  30. state = slot_public(auth, "/projects", "projects")
  31. act(auth, state["create"], {"name": "cycle",
  32. "git_url": "https://example.com/cycle.git"})
  33. cycle_id = int([p["id"] for p in
  34. slot_public(auth, "/projects", "projects")["projects"]
  35. if p["name"] == "cycle"][0])
  36. print("== cycle: the project page ships the section and Refresh ==")
  37. cycle_state = slot_public(auth, "/projects/%d" % cycle_id, "project")
  38. check(cycle_state.get("refresh_understanding") is not None,
  39. "the cycle project's slot authors the Refresh action")
  40. page_html = http_get_page(auth, "/projects/%d" % cycle_id)
  41. check("Understanding" in page_html and "Refresh" in page_html,
  42. "the project page renders the Understanding section with its button")
  43. print("== cycle: seed a current document + notes, press Refresh ==")
  44. status, body = saddle_api("POST", "/api/saddle/understanding", fake_token,
  45. {"project": "cycle",
  46. "body": "# Cycle v1\n\nThe old map.",
  47. "commit_hash": "aaa1111", "task_id": 0})
  48. check(status == 200 and body.get("ok") is True,
  49. "seeding the first understanding via the API works")
  50. conn = db()
  51. conn.execute("insert into project_notes (project_id, source_task_id, body,"
  52. " created_at) values (?, 0, ?, strftime('%s','now'))",
  53. (cycle_id, "Cycle note: the fixtures live in test/bundles/."))
  54. conn.commit()
  55. conn.close()
  56. cycle_state = slot_public(auth, "/projects/%d" % cycle_id, "project")
  57. result = act(auth, cycle_state["refresh_understanding"], {})
  58. check(any(d.get("type") == "notify" and d.get("kind") == "success" for d in result),
  59. "pressing Refresh queues the explorer task")
  60. rows = db_rows("select id, title, status from tasks"
  61. " where title like 'Understanding refresh: cycle'")
  62. check(len(rows) == 1, "Refresh created exactly one task")
  63. refresh_id = rows[0][0]
  64. check(rows[0][1].startswith("Understanding refresh:"),
  65. "the refresh task's title carries the goal-24 prefix")
  66. check(rows[0][2] in ("queued", "assigned", "working"),
  67. "the refresh task is live (%s)" % rows[0][2])
  68. print("== cycle: the refresh task's OWN assignment payload ==")
  69. # The fake saddle asks when idle, so the dispatcher hands it the
  70. # refresh task: the payload the daemon builds from the brief AND
  71. # the understanding member must both be there.
  72. check(wait_for(lambda: payload_for(refresh_id) is not None or False,
  73. timeout=30, what="refresh assignment push") is not None,
  74. "the dispatcher assigns the refresh task to the saddle")
  75. payload = payload_for(refresh_id)
  76. brief = (payload.get("task") or {}).get("description") or ""
  77. check("Previous commit hash: aaa1111" in brief,
  78. "the assigned brief embeds the OLD commit hash")
  79. check("```markdown" in brief and "The old map." in brief,
  80. "the assigned brief embeds the OLD document")
  81. check("Cycle note: the fixtures live in test/bundles/." in brief,
  82. "the assigned brief embeds the project notes from other agents")
  83. check(payload.get("understanding") == "# Cycle v1\n\nThe old map.",
  84. "the assignment payload's understanding member is the current doc")
  85. check("discoveries" in payload and isinstance(payload.get("discoveries"), list),
  86. "the assignment payload carries the discoveries array alongside")
  87. print("== cycle: the explorer saves; the page flips ==")
  88. fake.push_results("done", summary="walked the history and updated the doc")
  89. check(wait_task_done(auth, refresh_id, timeout=30) is not None,
  90. "the refresh task completes")
  91. status, body = saddle_api("POST", "/api/saddle/understanding", fake_token,
  92. {"project": "cycle",
  93. "body": "# Cycle v2\n\nThe updated map.",
  94. "commit_hash": "bbb2222",
  95. "task_id": refresh_id})
  96. check(status == 200 and int(body.get("understanding_id", 0)) > 0,
  97. "the explorer's save_understanding POST lands")
  98. cycle_state = slot_public(auth, "/projects/%d" % cycle_id, "project")
  99. current = cycle_state.get("understanding_current") or {}
  100. check(current.get("body") == "# Cycle v2\n\nThe updated map."
  101. and current.get("commit_hash") == "bbb2222",
  102. "the page shows the NEW current document after the save")
  103. history = cycle_state.get("understanding_history") or []
  104. check([h.get("commit_hash") for h in history] == ["aaa1111"]
  105. and (history[0].get("body") or "").endswith("The old map."),
  106. "the page keeps the previous revision as a history row")
  107. page_html = http_get_page(auth, "/projects/%d" % cycle_id)
  108. check("stm-markdown=\"project.understanding_current.body\"" in page_html,
  109. "the rendered page binds the current document as markdown")
  110. print("== cycle: a second save makes the page show the newer ==")
  111. status, body = saddle_api("POST", "/api/saddle/understanding", fake_token,
  112. {"project": "cycle",
  113. "body": "# Cycle v3\n\nThe newest map.",
  114. "commit_hash": "ccc3333"})
  115. check(status == 200, "the second explorer POST lands")
  116. cycle_state = slot_public(auth, "/projects/%d" % cycle_id, "project")
  117. current = cycle_state.get("understanding_current") or {}
  118. check(current.get("body") == "# Cycle v3\n\nThe newest map.",
  119. "the page's current flips to the newest revision")
  120. check([h.get("commit_hash") for h in
  121. cycle_state.get("understanding_history") or []]
  122. == ["bbb2222", "aaa1111"],
  123. "the history grows newest-first with both older revisions")
  124. print("== cycle: the discoveries leg ==")
  125. status, body = saddle_api("POST", "/api/saddle/discovery", fake_token,
  126. {"body": "cycle fact: meson test -C builddir runs the suite",
  127. "task_id": refresh_id})
  128. check(status == 200 and int(body.get("discovery_id", 0)) > 0,
  129. "POST /api/saddle/discovery inserts and echoes the id")
  130. discovery_id = int(body["discovery_id"])
  131. discoveries_state = slot_public(auth, "/discoveries", "discoveries")
  132. listed = [r for r in discoveries_state.get("discoveries") or []
  133. if r.get("body") == "cycle fact: meson test -C builddir runs the suite"]
  134. check(len(listed) == 1, "the /discoveries page lists the new row")
  135. home_state = slot_public(auth, "/", "dashboard")
  136. dashboard = [r for r in home_state.get("discoveries") or []
  137. if r.get("body") == "cycle fact: meson test -C builddir runs the suite"]
  138. check(len(dashboard) == 1, "the dashboard shows the discovery")
  139. status, body = saddle_api("POST", "/api/saddle/discovery/update", fake_token,
  140. {"id": discovery_id,
  141. "body": "cycle fact: meson test -C builddir runs the suite (also task-log-js)"})
  142. check(status == 200 and body.get("ok") is True, "update-by-id answers ok")
  143. discoveries_state = slot_public(auth, "/discoveries", "discoveries")
  144. rows_now = [r for r in discoveries_state.get("discoveries") or []
  145. if r.get("id") == str(discovery_id)]
  146. check(len(rows_now) == 1
  147. and "(also task-log-js)" in rows_now[0].get("body", ""),
  148. "the page shows the refined body in place (no duplicate row)")
  149. # A fresh assignment carries the refined discovery.
  150. payload_count = len(fake.start_payloads)
  151. create_queued(auth, {"project_id": str(cycle_id),
  152. "title": "Cycle dispatch",
  153. "priority": "2"})
  154. check(wait_for(lambda: len(fake.start_payloads) > payload_count or False,
  155. timeout=30, what="cycle /start push") is not None,
  156. "the dispatcher pushes the cycle task")
  157. shipped = [r.get("body", "") for r in
  158. fake.start_payloads[-1].get("discoveries") or []]
  159. check(any("(also task-log-js)" in b for b in shipped),
  160. "a fresh assignment payload carries the refined discovery")
  161. check(fake.start_payloads[-1].get("understanding")
  162. == "# Cycle v3\n\nThe newest map.",
  163. "the fresh assignment also carries the newest understanding")