From 0c3156e8d6041ac66aafd517cd6b17b99e4678cc Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Sat, 3 Oct 2026 19:50:28 -0700 Subject: [PATCH] fix: do not link a cluster repair target A non-URL target such as cluster:... was rendered as a link to itself and hid action.pr and action.url. Link those first, and leave cluster targets unlinked. Signed-off-by: Sebastien Tardif --- scripts/repair-dashboard.mjs | 23 ++++++++++-------- test/repair-dashboard.test.mjs | 43 ++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 10 deletions(-) diff --git a/scripts/repair-dashboard.mjs b/scripts/repair-dashboard.mjs index 16418f38e0c..6968ce084ae 100644 --- a/scripts/repair-dashboard.mjs +++ b/scripts/repair-dashboard.mjs @@ -518,8 +518,17 @@ function runLink(record) { return record.run_url ? link(record.run_id ?? "run", record.run_url) : "_none_"; } +function githubIssueOrPullLink(value) { + const url = String(value ?? ""); + const match = url.match(/^https:\/\/github\.com\/[^/]+\/[^/]+\/(?:issues|pull)\/(\d+)$/); + return match ? link(`#${match[1]}`, url) : ""; +} + function targetLink(record, action) { + const preferred = githubIssueOrPullLink(action.pr) || githubIssueOrPullLink(action.url); + if (preferred) return preferred; const target = String(action.target ?? ""); + if (!target || target.startsWith("cluster:")) return ""; const match = target.match(/^https:\/\/github\.com\/([^/]+\/[^/]+)\/(issues|pull)\/(\d+)/); if (match) return link(`#${match[3]}`, target); const shorthand = target.match(/^#(\d+)$/); @@ -530,7 +539,7 @@ function targetLink(record, action) { const segment = repairActionTargetsPullRequest(action) ? "pull" : "issues"; return link(target, `https://github.com/${repo}/${segment}/${shorthand[1]}`); } - return target ? link(target, target) : ""; + return ""; } function githubItemUrlForNumber(value, number) { @@ -570,15 +579,9 @@ function ownerActionRow(row) { } function ownerItemLink(record, action) { - const target = targetLink(record, action); - if (target) return target; - for (const value of [action.pr, action.url]) { - const match = String(value ?? "").match( - /^https:\/\/github\.com\/[^/]+\/[^/]+\/(?:issues|pull)\/(\d+)$/, - ); - if (match) return link(`#${match[1]}`, value); - } - return ""; + const preferred = githubIssueOrPullLink(action.pr) || githubIssueOrPullLink(action.url); + if (preferred) return preferred; + return targetLink(record, action); } function repoLink(record) { diff --git a/test/repair-dashboard.test.mjs b/test/repair-dashboard.test.mjs index 314cef506a1..9a1b9e67a9f 100644 --- a/test/repair-dashboard.test.mjs +++ b/test/repair-dashboard.test.mjs @@ -76,6 +76,49 @@ test("repair dashboard links shorthand targets through the source repo", () => { ); }); +test("repair dashboard links a pull or issue before a cluster target", () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "clawsweeper-state-cluster-")); + const runsDir = path.join(root, "results", "runs"); + fs.mkdirSync(runsDir, { recursive: true }); + fs.writeFileSync( + path.join(runsDir, "run.json"), + JSON.stringify({ + repo: "openclaw/openclaw", + cluster_id: "issue-openclaw-openclaw-5", + run_id: "55", + run_url: "https://github.com/openclaw/clawsweeper/actions/runs/55", + workflow_conclusion: "success", + published_at: "2026-05-02T19:00:00.000Z", + needs_human: ["choose the real item"], + fix_actions: [ + { + target: "cluster:issue-openclaw-openclaw-5", + pr: "https://github.com/openclaw/openclaw/pull/9", + url: "https://github.com/openclaw/openclaw/issues/5", + status: "blocked", + reason: "cluster target is not a url", + }, + ], + apply_actions: [ + { + target: "cluster:issue-openclaw-openclaw-8", + action: "close_duplicate", + url: "https://github.com/openclaw/openclaw/issues/8", + status: "executed", + title: "use the issue url", + closed_at: "2026-05-02T19:01:00.000Z", + }, + ], + }), + "utf8", + ); + + const dashboard = renderRepairDashboard(root); + assert.match(dashboard, /\[#9\]\(https:\/\/github\.com\/openclaw\/openclaw\/pull\/9\)/); + assert.match(dashboard, /\[#8\]\(https:\/\/github\.com\/openclaw\/openclaw\/issues\/8\)/); + assert.doesNotMatch(dashboard, /\]\(cluster:/); +}); + test("repair dashboard groups owner action lanes by operator bucket", () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "clawsweeper-state-owner-")); const runsDir = path.join(root, "results", "runs");