Commit ed8e8e0de10452231f6b770d758f2485640fca9c

Authored by graphlemon
1 parent bc3e7f6a

graphlemon: code_diff artifact for task 773a7a08-b295-4ba3-b506-196a21403d8b

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