Преглед изворни кода

fix: at most ONE pending completion-family block renders (stale pre-dedupe twins hidden), and the verdict answers every pending twin — the review shows once, the report card only after acceptance

clanker пре 1 месец
родитељ
комит
4884259da9
3 измењених фајлова са 53 додато и 5 уклоњено
  1. 15 0
      src/Services/GoalService.vala
  2. 14 5
      src/entrypoints/GoalDetailEntrypoint.vala
  3. 24 0
      test/web_stack_test.py

+ 15 - 0
src/Services/GoalService.vala

@@ -578,6 +578,21 @@ namespace ClankerSaddle.Services {
 
             yield tasks.answer_block(block_id, answer, answered_by);
 
+            if (is_completion_row(((!)block))) {
+                // Pre-dedupe twins: answer EVERY pending completion-family
+                // row with the same verdict — a lingering twin must never
+                // re-render the review after it was decided.
+                var run_for_twins = yield goal_run_task(((!)goal).id);
+                if (run_for_twins != null) {
+                    foreach (var twin in yield tasks.list_blocks(((!)run_for_twins).id)) {
+                        if (twin.id != block_id && twin.answer == null
+                                && is_completion_row(twin)) {
+                            yield tasks.answer_block(twin.id, answer, answered_by);
+                        }
+                    }
+                }
+            }
+
             if (is_completion_row(((!)block))
                     && answer == NOTICE_REVIEW_APPROVED) {
                 // Approving the completion closes the goal agent's

+ 14 - 5
src/entrypoints/GoalDetailEntrypoint.vala

@@ -474,15 +474,24 @@ namespace ClankerSaddle {
             var open_sink = blocks_open ?? new Vector<GoalBlockView>();
             var answered_sink = blocks_answered ?? new Vector<GoalBlockView>();
             var block_array = blocks.to_array();
+            GoalBlockView? pending_review = null;
             for (var index = block_array.length - 1; index >= 0; index--) {
                 var view = (GoalBlockView) block_array[index];
                 if (view.answered) {
                     answered_sink.add(view);
-                } else if (!view.is_plan_review) {
-                    // The PENDING completion review rides the attention
-                    // blocks like any other (the rich review form with
-                    // the highlight-remarks queue); the dedicated
-                    // Completion review card is the terminal summary.
+                } else if (view.is_plan_review) {
+                    // rendered by the plan section
+                } else if (view.is_completion_review) {
+                    // AT MOST ONE pending completion-family block renders
+                    // (the rich review form): pre-dedupe twins may exist in
+                    // stale data — the first pending row owns the review,
+                    // any others stay hidden until the verdict answers
+                    // them all.
+                    if (pending_review == null) {
+                        pending_review = view;
+                        open_sink.add(view);
+                    }
+                } else {
                     open_sink.add(view);
                 }
             }

+ 24 - 0
test/web_stack_test.py

@@ -5759,6 +5759,22 @@ def bundle_goals(ctx):
     check(state["completion_pending"] is True and len(review_family_in_open) == 1,
           "the pending completion review renders ONCE — as the rich attention block")
     fake.pending_block = None
+    # Stale-twin healing (live incident shape): a pre-dedupe run left BOTH
+    # a completion-kind and a review-kind row pending — the page must
+    # render ONE review all the way through, and the final verdict must
+    # answer BOTH rows (asserted after the arc's accept below).
+    conn = db()
+    conn.execute(
+        "insert into task_blocks (task_id, question, type, body, created_at)"
+        " values (?, 'Goal completion review (stale twin)', 'review',"
+        " '# Stale twin body', strftime('%s','now'))", (run_task,))
+    conn.commit()
+    conn.close()
+    state = goal_state(goal1)
+    stale_open = [b for b in state.get("open_blocks", [])
+                  if b.get("is_completion_review")]
+    check(len(stale_open) == 1,
+          "a stale pre-dedupe twin renders NOTHING — one review only")
     # Plan 15 C4: the completion-accept hard gate — the live probe
     # flags still report repob's branch unpushed, so the operator's
     # accept REFUSES with origin behind.
@@ -5796,6 +5812,14 @@ def bundle_goals(ctx):
     check(wait_for(lambda: task_state(auth, run_task)["status"] == "done" or False,
                    timeout=30, what="goal run task closes") is not None,
           "approving the completion closes the goal-run task done")
+    conn = db()
+    twins_left = conn.execute(
+        "select count(*) from task_blocks where task_id = ?"
+        " and answer is null and type in ('completion','review')",
+        (run_task,)).fetchone()[0]
+    conn.close()
+    check(twins_left == 0,
+          "the verdict answered every pending completion-family row (stale twin included)")
 
     print("== plan 15 C3: the narrative derives from the recorded shape ==")
     # Content equivalence with the old projection on this run's exact