From a044b73df2312669a22174fcdb5716238068a80e Mon Sep 17 00:00:00 2001 From: graphlemon Date: Sat, 8 Aug 2026 11:53:41 +0800 Subject: [PATCH] graphlemon: auto-commit uncommitted changes for judgment-550c6f093701-main-checkout-drift --- frontend/app.js | 1 + frontend/style.css | 16 ++++++++++++++++ frontend/test_graph_console_smoke.js | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 0 deletions(-) diff --git a/frontend/app.js b/frontend/app.js index c2aa3ec..c746273 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -2104,6 +2104,7 @@ function mountGraphConsole() { } workGraphPanelsEl.innerHTML = panelsHtml; mountGraphVisualizerTooltips(workGraphPanelsEl); + mountGraphVisualizerEdges(workGraphPanelsEl); mountWorkNodeClickExpand(workGraphPanelsEl, { onNodeClick: onWorkNodeClick }); } diff --git a/frontend/style.css b/frontend/style.css index 1d4cac4..06b424b 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -136,6 +136,22 @@ table.data-table tr[data-task-id]:hover td { background: #f9fafb; } .work-node.status-waiting_for_quota::after { content: "🕐"; position: absolute; top: -8px; right: -8px; font-size: 14px; } .work-node.status-blocked, .work-node.status-failed { border-color: var(--status-blocked); background: #fef2f2; } .work-node.status-done { border-color: var(--status-done); background: #f0fdf4; } + +/* 2026-08-08 (Work Graph self-planning + loop-back): on_failure edges + (loop_back) drawn as lines over the existing row layout, plus a badge + for retry_self loops (which have no edge to draw -- see + mountGraphVisualizerEdges/countSelfLoopAttempts in app.js). The svg + sits absolutely over .work-graph (already position:relative) and + paints behind the node boxes since it appears earlier in the DOM (see + renderGraphVisualizerPanel) -- no z-index juggling needed. */ +.graph-edges-svg { position: absolute; top: 0; left: 0; pointer-events: none; overflow: visible; } +.edge-on-failure { stroke: #dc2626; stroke-width: 2; stroke-dasharray: 5 3; fill: none; } +.edge-label { font-size: 10px; fill: #dc2626; text-anchor: middle; paint-order: stroke; stroke: #fff; stroke-width: 3px; } +.loop-badge { + display: inline-block; margin-left: 6px; padding: 0 5px; border-radius: 9px; + background: #fee2e2; color: #dc2626; font-size: 10px; font-weight: 600; cursor: help; +} +.edge-sample { display: inline-block; width: 20px; height: 0; border-top: 2px dashed #dc2626; } .legend { display: flex; gap: 16px; flex-wrap: wrap; font-size: 12px; color: #6b7280; } .legend-item { display: flex; align-items: center; gap: 6px; } .legend-swatch { width: 14px; height: 14px; border-radius: 4px; border: 2px solid; } diff --git a/frontend/test_graph_console_smoke.js b/frontend/test_graph_console_smoke.js index 6d79da1..12772ef 100644 --- a/frontend/test_graph_console_smoke.js +++ b/frontend/test_graph_console_smoke.js @@ -121,6 +121,79 @@ new vm.Script(augmentedSrc, { filename: "app.js" }).runInContext(sandbox); assert.ok(graphConsoleHtml.includes("work-node status-running clickable"), "opted-in call adds the clickable class"); assert.ok(graphConsoleHtml.includes('data-clickable-node-exec-id="exec-1"'), "opted-in call adds the click-target id"); assert.ok(graphConsoleHtml.includes('
'), "opted-in call reserves a log-viewer-slot"); + assert.ok(graphConsoleHtml.includes('data-node-id="n1"'), "every node carries data-node-id for edge-drawing lookup"); +} + +// ---- renderGraphVisualizerPanel: Work Graph self-planning + loop-back +// (2026-08-08) -- retry_self badge and on_failure edge data. Real line +// positions require getBoundingClientRect (mountGraphVisualizerEdges), +// which this bare vm sandbox can't provide -- that part is manual-browser- +// verified only, same convention this file's own header note already +// documents for DOM-rendering functions in general. What CAN be +// mechanically checked here is the HTML-string generation itself: the +// badge text/count, and the data-on-failure-edges payload the real mount +// function would read. +{ + const runWithSelfLoop = { + org_node: "planner", run_id: "run-2", + graph_manifest: { nodes: [{ node_id: "benchmark", depends_on: [] }] }, + nodes: [{ node_id: "benchmark", node_exec_id: "exec-2", subagent_type: "planner", status: "reflecting" }], + loop_back_events: [ + { source_node_id: "benchmark", target_node_id: "benchmark", attempt_number: 1, feedback: ["missing scenario"] }, + { source_node_id: "benchmark", target_node_id: "benchmark", attempt_number: 2, feedback: ["still missing"] }, + ], + }; + const html = sandbox.renderGraphVisualizerPanel(runWithSelfLoop); + assert.ok(html.includes('class="loop-badge"'), "a node with self-loop events gets a loop-badge"); + assert.ok(html.includes(">↺ 2<"), "the badge shows the real count of self-loop attempts, not just a presence flag"); + // the attribute is always present (simpler than conditionally omitting + // it), just empty when there's no on_failure edge to draw. + assert.ok(html.includes('data-on-failure-edges="[]"'), "retry_self alone has no on_failure edges -- payload is an empty array"); + + const runWithoutLoop = { + org_node: "planner", run_id: "run-3", + graph_manifest: { nodes: [{ node_id: "brd", depends_on: [] }] }, + nodes: [{ node_id: "brd", node_exec_id: "exec-3", subagent_type: "planner", status: "done" }], + }; + const plainHtml = sandbox.renderGraphVisualizerPanel(runWithoutLoop); + assert.ok(!plainHtml.includes("loop-badge"), "a run with no loop_back_events at all (older tasks) renders no badge"); + + const runWithLoopBackEdge = { + org_node: "planner", run_id: "run-4", + graph_manifest: { + nodes: [{ node_id: "sdd", depends_on: [] }, { node_id: "benchmark", depends_on: ["sdd"] }], + edges: [{ from: "benchmark", to: "sdd", edge_type: "on_failure" }], + }, + nodes: [ + { node_id: "sdd", node_exec_id: "exec-sdd", subagent_type: "planner", status: "done" }, + { node_id: "benchmark", node_exec_id: "exec-bm", subagent_type: "planner", status: "running" }, + ], + loop_back_events: [ + { source_node_id: "benchmark", target_node_id: "sdd", attempt_number: 1, feedback: ["x"] }, + ], + }; + const loopBackHtml = sandbox.renderGraphVisualizerPanel(runWithLoopBackEdge); + assert.ok(loopBackHtml.includes("data-on-failure-edges"), "a real on_failure edge emits the edges payload for mountGraphVisualizerEdges to read"); + assert.ok(loopBackHtml.includes(""from":"benchmark""), "the payload's escaped JSON names the real source node"); + assert.ok(loopBackHtml.includes(""triggerCount":1"), "the payload counts real triggers from loop_back_events, not just declares the edge"); + assert.ok(loopBackHtml.includes("loop_back(失败回路)"), "the legend explains the on_failure line when one is present"); + + const noEdgeLegendHtml = sandbox.renderGraphVisualizerPanel(runWithSelfLoop); + assert.ok(!noEdgeLegendHtml.includes("loop_back(失败回路)"), "no on_failure edge -- legend stays unchanged, no dangling explanation"); +} + +// ---- countSelfLoopAttempts ---- +{ + const run = { + loop_back_events: [ + { source_node_id: "a", target_node_id: "a", attempt_number: 1 }, + { source_node_id: "a", target_node_id: "b", attempt_number: 1 }, // loop_back, not a self-loop -- must not count + { source_node_id: "a", target_node_id: "a", attempt_number: 2 }, + ], + }; + assert.strictEqual(sandbox.countSelfLoopAttempts(run, "a"), 2, "counts only true self-loops (source===target===nodeId)"); + assert.strictEqual(sandbox.countSelfLoopAttempts(run, "b"), 0, "a node that was only ever a loop_back TARGET (not source) has zero self-loops"); + assert.strictEqual(sandbox.countSelfLoopAttempts({}, "a"), 0, "missing loop_back_events (older task) is treated as zero, not a crash"); } // ---- renderArtifactSummaryList (FR-5) ---- -- libgit2 0.21.4