mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-25 21:20:00 +00:00
Integrate reviewed legacy issue version classification repair
Preserve exact reviewed commits50a1e466c6and92cf192103. Retained source-6f3apw81 covers 51 parser and label-flow tests, docs/mirror checks and discriminating parent/compatibility controls. No new product surface or runtime frontend change. Change-source: pulse-maintainer
This commit is contained in:
@@ -67,9 +67,18 @@ function normalizeVersion(value) {
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
function normalizeLegacyVersionValue(value) {
|
||||
const visible = String(value).trim().replace(/^[`*_]+/, "");
|
||||
// Keep an explicitly shared server/agent version (as in #1788), without
|
||||
// mining a sentence such as "unknown; agent version 6.5.0" for a number.
|
||||
return /^(?:v?\d+\.\d+\.\d+\b|server(?:[ \t]*\+[ \t]*pulse-agent)?[ \t]+v?\d+\.\d+\.\d+\b|(?:[a-z0-9._/-]+\/)?pulse:|pulse[-_])/i.test(visible)
|
||||
? normalizeVersion(visible) : null;
|
||||
}
|
||||
|
||||
function extractPulseVersion(title, body) {
|
||||
if (body) {
|
||||
const lines = body.split(/\r?\n/);
|
||||
// Hidden template examples and headings are not reporter evidence.
|
||||
const lines = stripHTMLComments(body).split(/\r?\n/);
|
||||
const versionHeading = lines.findIndex((line) =>
|
||||
/^#{1,6}[ \t]+Pulse[ \t]+version[ \t]*$/i.test(line)
|
||||
);
|
||||
@@ -82,32 +91,30 @@ function extractPulseVersion(title, body) {
|
||||
if (/^#{1,6}[ \t]+/.test(lines[i])) break;
|
||||
value.push(lines[i]);
|
||||
}
|
||||
return normalizeVersion(stripHTMLComments(value.join("\n")));
|
||||
return normalizeVersion(value.join("\n"));
|
||||
}
|
||||
for (let i = 0; i < lines.length; i += 1) {
|
||||
const line = lines[i] || "";
|
||||
if (/pulse\s*(\||-)?\s*version/i.test(line)) {
|
||||
const inlineVersion = normalizeVersion(line);
|
||||
if (inlineVersion) return inlineVersion;
|
||||
const line = lines[i].replace(/\*\*|__/g, "")
|
||||
.replace(/^[ \t]*(?:#{1,6}[ \t]+|[-*][ \t]+)/, "").trim();
|
||||
const field = line.match(
|
||||
/^Pulse[ \t]*(?:[|-][ \t]*)?version(?:[ \t]*[:|][ \t]*|[ \t]+|$)(.*)$/i
|
||||
);
|
||||
if (!field) continue;
|
||||
|
||||
for (let j = i + 1; j < Math.min(i + 6, lines.length); j += 1) {
|
||||
const nearby = (lines[j] || "").trim();
|
||||
if (!nearby) continue;
|
||||
const nearbyVersion = normalizeVersion(nearby);
|
||||
if (nearbyVersion) return nearbyVersion;
|
||||
}
|
||||
// Legacy inline/standalone fields have the same authority as the form.
|
||||
// Unknown or incomplete must not borrow an agent/platform version or
|
||||
// an upgrade's starting version from the title.
|
||||
const inlineValue = field[1].trim();
|
||||
if (inlineValue) return normalizeLegacyVersionValue(inlineValue);
|
||||
for (let j = i + 1; j < lines.length; j += 1) {
|
||||
const value = lines[j].trim();
|
||||
if (!value || /^(?:```|~~~)[\w-]*$/.test(value)) continue;
|
||||
// Accept a version or image reference as the first visible value,
|
||||
// not a neighbouring heading, named field, log or prose paragraph.
|
||||
return normalizeLegacyVersionValue(value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const headingMatch = body.match(
|
||||
/#+\s*Pulse version[\s\S]{0,80}?(\bv?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?\b)/i
|
||||
);
|
||||
if (headingMatch) return normalizeVersion(headingMatch[1]);
|
||||
|
||||
const legacyMatch = body.match(
|
||||
/pulse\s*\|?\s*version[^\n]*?(\bv?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?\b)/i
|
||||
);
|
||||
if (legacyMatch) return normalizeVersion(legacyMatch[1]);
|
||||
}
|
||||
|
||||
return normalizeVersion(title);
|
||||
|
||||
@@ -551,6 +551,102 @@ test("structured Pulse version never falls back to an upgrade source or agent ve
|
||||
assert.equal(extractPulseVersion("bug on v5.1.35", "### Pulse version"), null);
|
||||
});
|
||||
|
||||
test("legacy version fields keep unknown and incomplete values authoritative", () => {
|
||||
const { extractPulseVersion } = triage.internals;
|
||||
for (const field of ["Pulse version:", "**Pulse Version:**", "Pulse | Version", "## Pulse - Version"]) {
|
||||
for (const value of ["unknown", "_No response_", "6.4", "6"]) {
|
||||
for (const neighbour of ["### Agent version\n6.5.0", "Operating system: Debian 13.0.0"]) {
|
||||
const body = `${field}\n${value}\n\n${neighbour}`;
|
||||
assert.equal(extractPulseVersion("upgrade from v6.4.5", body), null, body);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const value of ["unknown", "6.4", "_No response_"]) {
|
||||
assert.equal(extractPulseVersion("upgrade from v6.4.5", `Pulse version: ${value}; Agent version 6.5.0`), null);
|
||||
}
|
||||
});
|
||||
|
||||
test("legacy version fields stop at their first visible value", () => {
|
||||
const { extractPulseVersion } = triage.internals;
|
||||
for (const value of [
|
||||
"### Agent version\n6.5.0",
|
||||
"Agent version: 6.5.0",
|
||||
"**OS / environment:** Debian 13.0.0",
|
||||
"Agent version\n6.5.0",
|
||||
"Not available; the agent runs 6.5.0",
|
||||
"```text\nunknown\n```\n### Agent version\n6.5.0",
|
||||
"",
|
||||
]) {
|
||||
assert.equal(extractPulseVersion("upgrade from v6.4.5", `Pulse | Version\n\n${value}`), null, value);
|
||||
}
|
||||
// Prose mentioning a field is not itself a version declaration.
|
||||
assert.equal(extractPulseVersion("Bug", "I cannot find the Pulse version.\nAgent version: 6.5.0"), null);
|
||||
});
|
||||
|
||||
test("legacy version extraction ignores hidden template versions and headings", () => {
|
||||
const { extractPulseVersion } = triage.internals;
|
||||
assert.equal(extractPulseVersion("install", "Pulse version: unknown <!-- example: v6.4.5 -->"), null);
|
||||
assert.equal(extractPulseVersion("upgrade from v6.4.5", "<!-- ### Pulse version\n6.4.5 -->\nPulse version: unknown"), null);
|
||||
assert.equal(extractPulseVersion("Bug", "Pulse | Version\n<!-- example\n6.4.5\n-->\nV6.5.0-rc.1"), "6.5.0-rc.1");
|
||||
assert.equal(extractPulseVersion("Bug", "<!-- ### Pulse version\n6.4.5 -->\n**Pulse Version:** 6.5.0"), "6.5.0");
|
||||
});
|
||||
|
||||
test("legacy version formats still classify actual version values", () => {
|
||||
const { extractPulseVersion } = triage.internals;
|
||||
for (const body of [
|
||||
"Pulse version: V6.5.0-rc.1",
|
||||
"**Pulse Version:** `v6.5.0-rc.1`",
|
||||
"- **Pulse version**: 6.5.0-rc.1",
|
||||
"Pulse | Version\n\nV6.5.0-rc.1\nAgent version: 6.4.5",
|
||||
"## Pulse - Version\r\n\r\n```text\r\n6.5.0-rc.1\r\n```\r\n",
|
||||
"Pulse version: rcourtman/pulse:6.5.0-rc.1",
|
||||
"Pulse | Version\n`rcourtman/pulse:6.5.0-rc.1`",
|
||||
]) {
|
||||
assert.equal(extractPulseVersion("upgrade from v6.4.5", body), "6.5.0-rc.1", body);
|
||||
}
|
||||
});
|
||||
|
||||
test("legacy joint server-agent versions remain reporter evidence", () => {
|
||||
const { extractPulseVersion } = triage.internals;
|
||||
const body = "**Pulse version:** server + pulse-agent v6.3.2 (Docker, `rcourtman/pulse:v6.3.2`)\n**Host OS:** Unraid 7.3.2\n**Agent:** Unified Agent v6.3.2, x86_64";
|
||||
assert.equal(extractPulseVersion("[Bug]: Unraid missing disks", body), "6.3.2");
|
||||
assert.equal(extractPulseVersion("Bug", "Pulse version: server V6.5.0"), "6.5.0");
|
||||
assert.equal(extractPulseVersion("Bug", "Pulse version: unknown; server + pulse-agent v6.3.2"), null);
|
||||
});
|
||||
|
||||
test("legacy unknown-version sync cannot fabricate release evidence or contact", async () => {
|
||||
for (const body of [
|
||||
"Pulse version: unknown\n### Agent version\n6.5.0",
|
||||
"Pulse | Version\nunknown\nOperating system: Debian 13.0.0",
|
||||
"Pulse version: unknown <!-- example: v6.4.5 -->",
|
||||
]) {
|
||||
const { github, calls } = createGithub();
|
||||
const issue = {
|
||||
number: 2805, title: "Upgrade from v6.4.5", body,
|
||||
labels: ["bug", "needs-retest-on-latest", "needs-decomposition", "operator-reviewed"].map(name => ({ name })),
|
||||
};
|
||||
await syncLabels({ github, context: createContext({ issue }), core: createCore() });
|
||||
assert.deepEqual(calls.addLabels.map(call => call.labels), [["needs-version-info"]]);
|
||||
assert.deepEqual(calls.createLabel.map(call => call.name), ["needs-version-info"]);
|
||||
assert.equal(calls.removeLabel.length, 0);
|
||||
assert.equal(calls.getLatestRelease.length, 0);
|
||||
assert.equal(calls.createComment.length, 0);
|
||||
}
|
||||
});
|
||||
|
||||
test("legacy known-version sync updates only classification labels", async () => {
|
||||
const { github, calls } = createGithub();
|
||||
const issue = {
|
||||
number: 2805, title: "Upgrade from v6.4.5", body: "**Pulse Version:** 6.5.0",
|
||||
labels: ["bug", "affects-6.4.5", "needs-version-info", "needs-retest-on-latest", "needs-decomposition", "operator-reviewed"].map(name => ({ name })),
|
||||
};
|
||||
await syncLabels({ github, context: createContext({ issue }), core: createCore() });
|
||||
assert.deepEqual(calls.addLabels.map(call => call.labels), [["affects-6.5.0"]]);
|
||||
assert.deepEqual(calls.removeLabel.map(call => call.name), ["affects-6.4.5", "needs-version-info"]);
|
||||
assert.equal(calls.getLatestRelease.length, 0);
|
||||
assert.equal(calls.createComment.length, 0);
|
||||
});
|
||||
|
||||
test("ambiguous upgrade version requests information without a retest comment", async () => {
|
||||
const { github, calls } = createGithub({ latestVersion: "6.4.1" });
|
||||
const issue = {
|
||||
|
||||
@@ -210,6 +210,15 @@ Version classification does not need the latest release and cannot establish
|
||||
relevant-fix availability. The synchronizer makes no release lookup, public
|
||||
comment, retest request or closure.
|
||||
|
||||
An explicit **Pulse version** field is authoritative even in a legacy inline
|
||||
or standalone report. An unknown or incomplete value must not be replaced by
|
||||
an upgrade's starting version in the title, a nearby agent/platform version or
|
||||
a hidden template example. A standalone legacy field uses only its first
|
||||
visible value, not later fields, logs or prose. A `needs-version-info` label is
|
||||
not proof that the full thread lacks the version; read existing evidence before
|
||||
asking only for a consequential remaining distinction. No refile or new
|
||||
diagnostics are required by version classification.
|
||||
|
||||
This boundary is deliberate: preserving an explicit reporter declaration is
|
||||
safe to automate, while deciding whether two observations are one root cause is
|
||||
a product and technical judgment.
|
||||
|
||||
@@ -210,6 +210,15 @@ Version classification does not need the latest release and cannot establish
|
||||
relevant-fix availability. The synchronizer makes no release lookup, public
|
||||
comment, retest request or closure.
|
||||
|
||||
An explicit **Pulse version** field is authoritative even in a legacy inline
|
||||
or standalone report. An unknown or incomplete value must not be replaced by
|
||||
an upgrade's starting version in the title, a nearby agent/platform version or
|
||||
a hidden template example. A standalone legacy field uses only its first
|
||||
visible value, not later fields, logs or prose. A `needs-version-info` label is
|
||||
not proof that the full thread lacks the version; read existing evidence before
|
||||
asking only for a consequential remaining distinction. No refile or new
|
||||
diagnostics are required by version classification.
|
||||
|
||||
This boundary is deliberate: preserving an explicit reporter declaration is
|
||||
safe to automate, while deciding whether two observations are one root cause is
|
||||
a product and technical judgment.
|
||||
|
||||
Reference in New Issue
Block a user