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"); | ... | ... |