Commit ed8e8e0de10452231f6b770d758f2485640fca9c
1 parent
bc3e7f6a
graphlemon: code_diff artifact for task 773a7a08-b295-4ba3-b506-196a21403d8b
Showing
1 changed file
with
162 additions
and
0 deletions
.graphlemon/runs/773a7a08-b295-4ba3-b506-196a21403d8b/code_diff.patch
0 → 100644
| 1 | +diff --git a/frontend/app.js b/frontend/app.js | ||
| 2 | +index 96ce34b..5a7da1d 100644 | ||
| 3 | +--- a/frontend/app.js | ||
| 4 | ++++ b/frontend/app.js | ||
| 5 | +@@ -90,11 +90,13 @@ const routes = { | ||
| 6 | + "#/bugs/new": { render: renderBugFeedback, mount: mountBugFeedback }, | ||
| 7 | + }; | ||
| 8 | + | ||
| 9 | +-// #/tasks/:task_id (v_task_detail) and #/skills/:skill_id/audit | ||
| 10 | +-// (v_skill_audit_log) are the only dynamic routes -- not worth a full | ||
| 11 | +-// path-matching router for two patterns. | ||
| 12 | ++// #/tasks/:task_id (v_task_detail), #/skills/:skill_id/audit | ||
| 13 | ++// (v_skill_audit_log), and #/artifacts/:artifact_id (v_artifact_browser, | ||
| 14 | ++// pre-selecting one artifact) are the only dynamic routes -- not worth a | ||
| 15 | ++// full path-matching router for three patterns. | ||
| 16 | + const TASK_DETAIL_RE = /^#\/tasks\/([^/]+)$/; | ||
| 17 | + const SKILL_AUDIT_RE = /^#\/skills\/([^/]+)\/audit$/; | ||
| 18 | ++const ARTIFACT_DETAIL_RE = /^#\/artifacts\/([^/]+)$/; | ||
| 19 | + | ||
| 20 | + let currentRouteCleanup = null; | ||
| 21 | + function registerRouteCleanup(fn) { | ||
| 22 | +@@ -113,12 +115,18 @@ function render() { | ||
| 23 | + if (!route) { | ||
| 24 | + const taskMatch = hash.match(TASK_DETAIL_RE); | ||
| 25 | + const skillMatch = hash.match(SKILL_AUDIT_RE); | ||
| 26 | ++ const artifactMatch = hash.match(ARTIFACT_DETAIL_RE); | ||
| 27 | + if (taskMatch) { | ||
| 28 | + route = { render: renderTaskDetail, mount: mountTaskDetail }; | ||
| 29 | + routeArgs = [taskMatch[1]]; | ||
| 30 | + } else if (skillMatch) { | ||
| 31 | + route = { render: renderSkillAuditLog, mount: mountSkillAuditLog }; | ||
| 32 | + routeArgs = [skillMatch[1]]; | ||
| 33 | ++ } else if (artifactMatch) { | ||
| 34 | ++ // Reuses v_artifact_browser itself (same render/mount as "#/artifacts") | ||
| 35 | ++ // with the artifact_id pre-selected -- no separate detail view. | ||
| 36 | ++ route = { render: renderArtifactBrowser, mount: mountArtifactBrowser }; | ||
| 37 | ++ routeArgs = [artifactMatch[1]]; | ||
| 38 | + } | ||
| 39 | + } | ||
| 40 | + if (!route) route = { render: () => "<h1>Not found</h1><p>" + hash + "</p>" }; | ||
| 41 | +@@ -1344,8 +1352,12 @@ const GC_CODE_ARTIFACT_TYPES = ["code_diff", "test_report"]; | ||
| 42 | + function renderArtifactSummaryList(artifacts, types, emptyText) { | ||
| 43 | + const filtered = artifacts.filter((a) => types.includes(a.artifact_type)).slice(0, 5); | ||
| 44 | + if (filtered.length === 0) return '<div class="hint">' + emptyText + "</div>"; | ||
| 45 | ++ // Each item links to v_artifact_browser (#/artifacts/:artifact_id) -- | ||
| 46 | ++ // click handlers are wired in mountArtifactsOverviewPanel after this HTML | ||
| 47 | ++ // is inserted into the DOM. This deliberately reuses the existing | ||
| 48 | ++ // browser/preview view rather than building a second content display. | ||
| 49 | + return '<ul class="simple-list">' + filtered.map((a) => | ||
| 50 | +- "<li><span>" + escapeHtml(a.artifact_type) + " · " + escapeHtml(a.file_path.split("/").pop()) + "</span><span>" | ||
| 51 | ++ '<li data-artifact-id="' + a.artifact_id + '" style="cursor:pointer;"><span>' + escapeHtml(a.artifact_type) + " · " + escapeHtml(a.file_path.split("/").pop()) + "</span><span>" | ||
| 52 | + + escapeHtml(a.validation_status) + "</span></li>" | ||
| 53 | + ).join("") + "</ul>"; | ||
| 54 | + } | ||
| 55 | +@@ -1397,6 +1409,15 @@ async function mountArtifactsOverviewPanel(container, projectId, { onSwitchToTas | ||
| 56 | + li.addEventListener("click", () => onSwitchToTask(li.dataset.runTaskId)); | ||
| 57 | + } | ||
| 58 | + } | ||
| 59 | ++ | ||
| 60 | ++ // Doc/code list items: jump to the existing v_artifact_browser detail | ||
| 61 | ++ // view (#/artifacts/:artifact_id) pre-selecting this artifact. "查看全部" | ||
| 62 | ++ // links above stay pointed at unfiltered #/artifacts per the FR-5 note. | ||
| 63 | ++ for (const li of container.querySelectorAll("li[data-artifact-id]")) { | ||
| 64 | ++ li.addEventListener("click", () => { | ||
| 65 | ++ window.location.hash = "#/artifacts/" + encodeURIComponent(li.dataset.artifactId); | ||
| 66 | ++ }); | ||
| 67 | ++ } | ||
| 68 | + } | ||
| 69 | + | ||
| 70 | + function renderGraphConsole() { | ||
| 71 | +@@ -1579,14 +1600,18 @@ function renderArtifactBrowser() { | ||
| 72 | + `; | ||
| 73 | + } | ||
| 74 | + | ||
| 75 | +-function mountArtifactBrowser() { | ||
| 76 | ++function mountArtifactBrowser(initialArtifactId) { | ||
| 77 | + const projectId = getCurrentProjectId(); | ||
| 78 | + if (!projectId) return; | ||
| 79 | + const listEl = document.getElementById("ab_list"); | ||
| 80 | + const previewEl = document.getElementById("ab_preview"); | ||
| 81 | + const typeFilterEl = document.getElementById("ab_type_filter"); | ||
| 82 | + const taskFilterEl = document.getElementById("ab_task_filter"); | ||
| 83 | +- let selectedArtifactId = null; | ||
| 84 | ++ // Seeded from the #/artifacts/:artifact_id route (v_graph_console's | ||
| 85 | ++ // Planner 产出总览 list links here). refreshList()'s existing fallback | ||
| 86 | ++ // below already handles an id that turns out invalid/filtered-out -- | ||
| 87 | ++ // falls back to the first artifact in the (possibly filtered) list. | ||
| 88 | ++ let selectedArtifactId = initialArtifactId || null; | ||
| 89 | + | ||
| 90 | + fetch("/api/tasks?project_id=" + encodeURIComponent(projectId)) | ||
| 91 | + .then((resp) => resp.json()) | ||
| 92 | +diff --git a/frontend/test_graph_console_smoke.js b/frontend/test_graph_console_smoke.js | ||
| 93 | +index 55c29eb..6d79da1 100644 | ||
| 94 | +--- a/frontend/test_graph_console_smoke.js | ||
| 95 | ++++ b/frontend/test_graph_console_smoke.js | ||
| 96 | +@@ -12,7 +12,12 @@ | ||
| 97 | + // calls this via `.map(renderGraphVisualizerPanel)`, which passes the | ||
| 98 | + // array index as a second argument; that must NOT be misread as an | ||
| 99 | + // options object with enableNodeClick=true) | ||
| 100 | +-// - renderArtifactSummaryList (FR-5: type grouping/truncation/empty state) | ||
| 101 | ++// - renderArtifactSummaryList (FR-5: type grouping/truncation/empty state, | ||
| 102 | ++// plus 2026-08-04's click-through-to-v_artifact_browser fix: each item | ||
| 103 | ++// now carries data-artifact-id) | ||
| 104 | ++// - ARTIFACT_DETAIL_RE (2026-08-04: #/artifacts/:artifact_id route that | ||
| 105 | ++// v_artifact_browser's own mountArtifactBrowser(initialArtifactId) uses | ||
| 106 | ++// to pre-select the clicked artifact -- see render()'s route dispatch) | ||
| 107 | + // | ||
| 108 | + // Run: node frontend/test_graph_console_smoke.js | ||
| 109 | + | ||
| 110 | +@@ -50,7 +55,14 @@ const sandbox = { | ||
| 111 | + vm.createContext(sandbox); | ||
| 112 | + | ||
| 113 | + const src = fs.readFileSync(path.join(__dirname, "app.js"), "utf8"); | ||
| 114 | +-new vm.Script(src, { filename: "app.js" }).runInContext(sandbox); | ||
| 115 | ++// `function ...` declarations at top level become sandbox globals | ||
| 116 | ++// automatically (Annex B global-object semantics), but top-level `const` | ||
| 117 | ++// (e.g. ARTIFACT_DETAIL_RE) stays lexically scoped to the script and never | ||
| 118 | ++// reaches the sandbox object -- explicitly re-export the one const this | ||
| 119 | ++// test needs, in the same script/lexical scope, rather than changing app.js's | ||
| 120 | ++// declaration style just for testability. | ||
| 121 | ++const augmentedSrc = src + "\nglobalThis.ARTIFACT_DETAIL_RE = ARTIFACT_DETAIL_RE;\n"; | ||
| 122 | ++new vm.Script(augmentedSrc, { filename: "app.js" }).runInContext(sandbox); | ||
| 123 | + | ||
| 124 | + // ---- computeGraphLayers: still the shared layering logic v_graph_console | ||
| 125 | + // leans on for FR-2/3, unchanged by this SDD but worth pinning down since | ||
| 126 | +@@ -114,9 +126,9 @@ new vm.Script(src, { filename: "app.js" }).runInContext(sandbox); | ||
| 127 | + // ---- renderArtifactSummaryList (FR-5) ---- | ||
| 128 | + { | ||
| 129 | + const artifacts = [ | ||
| 130 | +- { artifact_type: "brd", file_path: "docs/brd.md", validation_status: "passed" }, | ||
| 131 | +- { artifact_type: "code_diff", file_path: "src/x.py", validation_status: "pending" }, | ||
| 132 | +- { artifact_type: "sdd", file_path: "docs/sdd.md", validation_status: "passed" }, | ||
| 133 | ++ { artifact_id: "art-1", artifact_type: "brd", file_path: "docs/brd.md", validation_status: "passed" }, | ||
| 134 | ++ { artifact_id: "art-2", artifact_type: "code_diff", file_path: "src/x.py", validation_status: "pending" }, | ||
| 135 | ++ { artifact_id: "art-3", artifact_type: "sdd", file_path: "docs/sdd.md", validation_status: "passed" }, | ||
| 136 | + ]; | ||
| 137 | + const docsHtml = sandbox.renderArtifactSummaryList(artifacts, ["brd", "prd", "sdd", "benchmark"], "空"); | ||
| 138 | + assert.ok(docsHtml.includes("brd") && docsHtml.includes("sdd"), "docs list groups brd+sdd together"); | ||
| 139 | +@@ -124,6 +136,23 @@ new vm.Script(src, { filename: "app.js" }).runInContext(sandbox); | ||
| 140 | + | ||
| 141 | + const emptyHtml = sandbox.renderArtifactSummaryList([], ["brd"], "暂无文档类 Artifact"); | ||
| 142 | + assert.ok(emptyHtml.includes("暂无文档类 Artifact"), "empty group shows the given empty-state text"); | ||
| 143 | ++ | ||
| 144 | ++ // 2026-08-04 fix: each item must carry a real artifact_id so | ||
| 145 | ++ // mountArtifactsOverviewPanel can wire a click -> #/artifacts/:id | ||
| 146 | ++ // handler (see render()'s ARTIFACT_DETAIL_RE dispatch) instead of the | ||
| 147 | ++ // old bare, unclickable <li>. | ||
| 148 | ++ assert.ok(docsHtml.includes('data-artifact-id="art-1"'), "brd item carries its real artifact_id"); | ||
| 149 | ++ assert.ok(docsHtml.includes('data-artifact-id="art-3"'), "sdd item carries its real artifact_id"); | ||
| 150 | ++} | ||
| 151 | ++ | ||
| 152 | ++// ---- ARTIFACT_DETAIL_RE (2026-08-04 click-through fix) ---- | ||
| 153 | ++{ | ||
| 154 | ++ const match = "#/artifacts/abc-123".match(sandbox.ARTIFACT_DETAIL_RE); | ||
| 155 | ++ assert.ok(match, "#/artifacts/:id must match ARTIFACT_DETAIL_RE"); | ||
| 156 | ++ assert.strictEqual(match[1], "abc-123", "captured group must be the artifact_id"); | ||
| 157 | ++ | ||
| 158 | ++ assert.ok(!"#/artifacts".match(sandbox.ARTIFACT_DETAIL_RE), "bare #/artifacts (unfiltered browser) must not match -- stays a static route"); | ||
| 159 | ++ assert.ok(!"#/artifacts/abc/extra".match(sandbox.ARTIFACT_DETAIL_RE), "extra path segments must not match"); | ||
| 160 | + } | ||
| 161 | + | ||
| 162 | + console.log("frontend/test_graph_console_smoke.js: all assertions passed"); |