Explorar el Código

Add the knowledge_cycle smoke bundle; fix discoveries.html ASCII

knowledge_cycle (goal 24 task 6) drives the whole understanding cycle
through the real app plus the scripted fake saddle, cross-cutting what
the per-repo bundles could only check in isolation: press Refresh, read
the refresh task's OWN assignment payload (brief embeds the old commit
hash, the old document and the project notes; the understanding member
rides alongside; the discoveries array is present), POST
/api/saddle/understanding, watch the project page flip to the new
current document with the old revision kept as a history row, then POST
again and see the page show the newer one. The discoveries leg: POST
via the API, the /discoveries page and the dashboard list it,
update-by-id refines it in place, and a fresh assignment payload
carries the refined row plus the newest understanding.

discoveries.html carried a U+2014 em-dash inside a script body, which
test_template_ascii.py rejects (the entity-corruption guard); replaced
with an ASCII comma. Registered the bundle in BUNDLES_ORDER.
saddle-Clanker-Saddle hace 4 semanas
padre
commit
b410ba4c30
Se han modificado 3 ficheros con 181 adiciones y 2 borrados
  1. 1 1
      src/pages/discoveries.html
  2. 179 0
      test/bundles/knowledge_cycle.py
  3. 1 1
      test/harness.py

+ 1 - 1
src/pages/discoveries.html

@@ -8,7 +8,7 @@
     // selects: the Statum entrypoint fetch never carries the page's
     // own query string to the server, so the box filters the
     // rendered rows directly (case-insensitive substring over the
-    // body — the same search the daemon's search_discoveries tool
+    // body, the same search the daemon's search_discoveries tool
     // performs server-side). A `?q=` on a direct entrypoint hit
     // pre-seeds the box (DiscoveriesEntrypoint reads it).
     if (window.statum) {

+ 179 - 0
test/bundles/knowledge_cycle.py

@@ -0,0 +1,179 @@
+"""The knowledge-cycle bundle (goal 24 task 6): ONE end-to-end pass
+through the understanding and discoveries cycles, cross-cutting the
+surfaces the per-repo bundles could only check in isolation.
+
+Understanding: press Refresh on a project → the created task's OWN
+assignment payload (what the daemon actually receives) carries the
+brief's embedded old hash / old document / project notes AND the
+`understanding` member with the current revision → the saddle POSTs
+/api/saddle/understanding → the project page shows the new current
+document and a history row → a second POST makes the page show the
+newer one.
+
+Discoveries: POST via the API → the /discoveries page lists it and the
+dashboard shows it → update-by-id refines it → a fresh assignment
+payload carries it.
+"""
+from harness import *  # noqa: F401,F403 — the shared wire helpers
+
+
+def run(ctx):
+    auth, fake, fake_id, fake_token = ctx.auth, ctx.fake, ctx.fake_id, ctx.fake_token
+
+    def db_rows(sql, params=()):
+        conn = db()
+        try:
+            return conn.execute(sql, params).fetchall()
+        finally:
+            conn.close()
+
+    def payload_for(task_id):
+        for payload in reversed(fake.start_payloads):
+            if int((payload.get("task") or {}).get("id") or 0) == int(task_id):
+                return payload
+        return None
+
+    # A dedicated project so the cycle owns every row it asserts on.
+    state = slot_public(auth, "/projects", "projects")
+    act(auth, state["create"], {"name": "cycle",
+                                "git_url": "https://example.com/cycle.git"})
+    cycle_id = int([p["id"] for p in
+                    slot_public(auth, "/projects", "projects")["projects"]
+                    if p["name"] == "cycle"][0])
+
+    print("== cycle: the project page ships the section and Refresh ==")
+    cycle_state = slot_public(auth, "/projects/%d" % cycle_id, "project")
+    check(cycle_state.get("refresh_understanding") is not None,
+          "the cycle project's slot authors the Refresh action")
+    page_html = http_get_page(auth, "/projects/%d" % cycle_id)
+    check("Understanding" in page_html and "Refresh" in page_html,
+          "the project page renders the Understanding section with its button")
+
+    print("== cycle: seed a current document + notes, press Refresh ==")
+    status, body = saddle_api("POST", "/api/saddle/understanding", fake_token,
+                              {"project": "cycle",
+                               "body": "# Cycle v1\n\nThe old map.",
+                               "commit_hash": "aaa1111", "task_id": 0})
+    check(status == 200 and body.get("ok") is True,
+          "seeding the first understanding via the API works")
+    conn = db()
+    conn.execute("insert into project_notes (project_id, source_task_id, body,"
+                 " created_at) values (?, 0, ?, strftime('%s','now'))",
+                 (cycle_id, "Cycle note: the fixtures live in test/bundles/."))
+    conn.commit()
+    conn.close()
+
+    cycle_state = slot_public(auth, "/projects/%d" % cycle_id, "project")
+    result = act(auth, cycle_state["refresh_understanding"], {})
+    check(any(d.get("type") == "notify" and d.get("kind") == "success" for d in result),
+          "pressing Refresh queues the explorer task")
+    rows = db_rows("select id, title, status from tasks"
+                   " where title like 'Understanding refresh: cycle'")
+    check(len(rows) == 1, "Refresh created exactly one task")
+    refresh_id = rows[0][0]
+    check(rows[0][1].startswith("Understanding refresh:"),
+          "the refresh task's title carries the goal-24 prefix")
+    check(rows[0][2] in ("queued", "assigned", "working"),
+          "the refresh task is live (%s)" % rows[0][2])
+
+    print("== cycle: the refresh task's OWN assignment payload ==")
+    # The fake saddle asks when idle, so the dispatcher hands it the
+    # refresh task: the payload the daemon builds from the brief AND
+    # the understanding member must both be there.
+    check(wait_for(lambda: payload_for(refresh_id) is not None or False,
+                   timeout=30, what="refresh assignment push") is not None,
+          "the dispatcher assigns the refresh task to the saddle")
+    payload = payload_for(refresh_id)
+    brief = (payload.get("task") or {}).get("description") or ""
+    check("Previous commit hash: aaa1111" in brief,
+          "the assigned brief embeds the OLD commit hash")
+    check("```markdown" in brief and "The old map." in brief,
+          "the assigned brief embeds the OLD document")
+    check("Cycle note: the fixtures live in test/bundles/." in brief,
+          "the assigned brief embeds the project notes from other agents")
+    check(payload.get("understanding") == "# Cycle v1\n\nThe old map.",
+          "the assignment payload's understanding member is the current doc")
+    check("discoveries" in payload and isinstance(payload.get("discoveries"), list),
+          "the assignment payload carries the discoveries array alongside")
+
+    print("== cycle: the explorer saves; the page flips ==")
+    fake.push_results("done", summary="walked the history and updated the doc")
+    check(wait_task_done(auth, refresh_id, timeout=30) is not None,
+          "the refresh task completes")
+    status, body = saddle_api("POST", "/api/saddle/understanding", fake_token,
+                              {"project": "cycle",
+                               "body": "# Cycle v2\n\nThe updated map.",
+                               "commit_hash": "bbb2222",
+                               "task_id": refresh_id})
+    check(status == 200 and int(body.get("understanding_id", 0)) > 0,
+          "the explorer's save_understanding POST lands")
+    cycle_state = slot_public(auth, "/projects/%d" % cycle_id, "project")
+    current = cycle_state.get("understanding_current") or {}
+    check(current.get("body") == "# Cycle v2\n\nThe updated map."
+          and current.get("commit_hash") == "bbb2222",
+          "the page shows the NEW current document after the save")
+    history = cycle_state.get("understanding_history") or []
+    check([h.get("commit_hash") for h in history] == ["aaa1111"]
+          and (history[0].get("body") or "").endswith("The old map."),
+          "the page keeps the previous revision as a history row")
+    page_html = http_get_page(auth, "/projects/%d" % cycle_id)
+    check("stm-markdown=\"project.understanding_current.body\"" in page_html,
+          "the rendered page binds the current document as markdown")
+
+    print("== cycle: a second save makes the page show the newer ==")
+    status, body = saddle_api("POST", "/api/saddle/understanding", fake_token,
+                              {"project": "cycle",
+                               "body": "# Cycle v3\n\nThe newest map.",
+                               "commit_hash": "ccc3333"})
+    check(status == 200, "the second explorer POST lands")
+    cycle_state = slot_public(auth, "/projects/%d" % cycle_id, "project")
+    current = cycle_state.get("understanding_current") or {}
+    check(current.get("body") == "# Cycle v3\n\nThe newest map.",
+          "the page's current flips to the newest revision")
+    check([h.get("commit_hash") for h in
+           cycle_state.get("understanding_history") or []]
+          == ["bbb2222", "aaa1111"],
+          "the history grows newest-first with both older revisions")
+
+    print("== cycle: the discoveries leg ==")
+    status, body = saddle_api("POST", "/api/saddle/discovery", fake_token,
+                              {"body": "cycle fact: meson test -C builddir runs the suite",
+                               "task_id": refresh_id})
+    check(status == 200 and int(body.get("discovery_id", 0)) > 0,
+          "POST /api/saddle/discovery inserts and echoes the id")
+    discovery_id = int(body["discovery_id"])
+    discoveries_state = slot_public(auth, "/discoveries", "discoveries")
+    listed = [r for r in discoveries_state.get("discoveries") or []
+              if r.get("body") == "cycle fact: meson test -C builddir runs the suite"]
+    check(len(listed) == 1, "the /discoveries page lists the new row")
+    home_state = slot_public(auth, "/", "dashboard")
+    dashboard = [r for r in home_state.get("discoveries") or []
+                 if r.get("body") == "cycle fact: meson test -C builddir runs the suite"]
+    check(len(dashboard) == 1, "the dashboard shows the discovery")
+
+    status, body = saddle_api("POST", "/api/saddle/discovery/update", fake_token,
+                              {"id": discovery_id,
+                               "body": "cycle fact: meson test -C builddir runs the suite (also task-log-js)"})
+    check(status == 200 and body.get("ok") is True, "update-by-id answers ok")
+    discoveries_state = slot_public(auth, "/discoveries", "discoveries")
+    rows_now = [r for r in discoveries_state.get("discoveries") or []
+                if r.get("id") == str(discovery_id)]
+    check(len(rows_now) == 1
+          and "(also task-log-js)" in rows_now[0].get("body", ""),
+          "the page shows the refined body in place (no duplicate row)")
+
+    # A fresh assignment carries the refined discovery.
+    payload_count = len(fake.start_payloads)
+    create_queued(auth, {"project_id": str(cycle_id),
+                         "title": "Cycle dispatch",
+                         "priority": "2"})
+    check(wait_for(lambda: len(fake.start_payloads) > payload_count or False,
+                   timeout=30, what="cycle /start push") is not None,
+          "the dispatcher pushes the cycle task")
+    shipped = [r.get("body", "") for r in
+               fake.start_payloads[-1].get("discoveries") or []]
+    check(any("(also task-log-js)" in b for b in shipped),
+          "a fresh assignment payload carries the refined discovery")
+    check(fake.start_payloads[-1].get("understanding")
+          == "# Cycle v3\n\nThe newest map.",
+          "the fresh assignment also carries the newest understanding")

+ 1 - 1
test/harness.py

@@ -1324,7 +1324,7 @@ BUNDLES_ORDER = ("pages", "dispatch", "subtasks", "lineage",
                  "allocation", "lifecycle", "answers", "push", "goals",
                  "replay", "goalui", "forcewake", "users", "stacks", "notes",
                  "resurrect", "origins", "parity", "understanding",
-                 "discoveries")
+                 "discoveries", "knowledge_cycle")
 
 
 def run_bundle(name):