mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-26 13:39:47 +00:00
04ac669a6c
Failed-install forms accept attempted release asset names, but archive and platform suffixes currently become part of the affected-version label. Recognise the published server archive naming shapes, keep companion binaries and verification files out of server version evidence, and preserve unknown fields, bare prerelease qualifiers, Community-owned labels and the live-state fence. Reproduce the actual silent label consumer on five affected inputs and qualify archive, formatting and adverse controls without reporter collection or public contact. Contract-Neutral: Silent issue classification and regression controls only; no product runtime, subsystem shape, rendered product surface, dependency, grant or public-contact behaviour change. Change-source: pulse-maintainer
1777 lines
85 KiB
JavaScript
1777 lines
85 KiB
JavaScript
const test = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
|
|
const triage = require("./issue-version-triage.cjs");
|
|
|
|
// GitHub issue forms do not submit markdown-only guidance as report fields.
|
|
// Detailed, optional guidance belongs immediately before the input it helps;
|
|
// test that location rather than requiring every reader to see it expanded.
|
|
function fieldGuidance(form, id) {
|
|
const blocks = form.split(/(?=^ - type: )/m).slice(1);
|
|
const index = blocks.findIndex(block => block.includes(` id: ${id}\n`));
|
|
assert.ok(index >= 0, `missing input ${id}`);
|
|
const preceding = blocks[index - 1] || "";
|
|
return (preceding.startsWith(" - type: markdown\n") ? preceding : "") + blocks[index];
|
|
}
|
|
|
|
for (const [name, summary, evidence] of [
|
|
["bug_report.yml", "bug_description", "logs"],
|
|
["v6_rc_feedback.yml", "summary", "evidence"],
|
|
]) {
|
|
test(`specialist intake guidance stays optional and beside its input: ${name}`, () => {
|
|
const form = fs.readFileSync(path.resolve(__dirname, "../ISSUE_TEMPLATE", name), "utf8");
|
|
const blocks = form.split(/(?=^ - type: )/m).slice(1);
|
|
const disclosures = [];
|
|
for (const block of blocks) {
|
|
if (!block.includes("<details")) continue;
|
|
assert.ok(block.startsWith(" - type: markdown\n"), "details must not be input descriptions");
|
|
assert.doesNotMatch(block, /<details\s+[^>]*\bopen\b/);
|
|
const groups = [...block.matchAll(/<details>\s*<summary>([^<]+)<\/summary>([\s\S]*?)<\/details>/g)];
|
|
assert.equal(groups.length, (block.match(/<details/g) || []).length, "each disclosure must close");
|
|
for (const [, title, content] of groups) {
|
|
assert.ok(title.trim(), "keyboard focus needs a meaningful summary");
|
|
assert.ok(content.trim(), "a disclosure must retain its guidance");
|
|
disclosures.push(title);
|
|
}
|
|
}
|
|
assert.deepEqual(disclosures.sort(), [
|
|
"Failed upgrade: which attempt, stop and recovery?",
|
|
"Several hosts or clusters: preserve which names and IDs repeat",
|
|
"Notification delivery: Test versus ordinary alerts",
|
|
"Pulse CPU, memory or disk writes: comparable measurements",
|
|
"VM or LXC memory: guest versus hypervisor readings",
|
|
].sort());
|
|
for (const [id, titles] of [
|
|
["upgrade_context", ["Failed upgrade:"]],
|
|
[summary, ["Several hosts or clusters:"]],
|
|
[evidence, ["Notification delivery:", "Pulse CPU, memory or disk writes:", "VM or LXC memory:"]],
|
|
]) {
|
|
const guidance = fieldGuidance(form, id);
|
|
for (const title of titles) assert.ok(guidance.includes(`<summary>${title}`), `${id}: ${title}`);
|
|
}
|
|
// Collection and public-disclosure warnings are never hidden in a disclosure.
|
|
const introduction = blocks[0];
|
|
assert.doesNotMatch(introduction, /<details/);
|
|
assert.match(introduction, /do not run it during backups, freeze\/thaw or an unresponsive-host incident/);
|
|
assert.match(introduction, /Anything you attach here is public/);
|
|
const attachment = blocks.find(block => block.includes(` id: ${evidence}\n`));
|
|
assert.match(attachment, /Review files and screenshots locally/);
|
|
assert.doesNotMatch(attachment, /<details|For notification reports|For CPU, memory or disk-write reports/);
|
|
});
|
|
}
|
|
|
|
function createGithub({
|
|
latestVersion = "6.0.1",
|
|
existingLabels = new Set(),
|
|
existingComments = [],
|
|
issues = [],
|
|
currentIssue = null,
|
|
} = {}) {
|
|
const calls = {
|
|
createComment: [],
|
|
createLabel: [],
|
|
getLabel: [],
|
|
getLatestRelease: [],
|
|
paginate: [],
|
|
addLabels: [],
|
|
removeLabel: [],
|
|
getIssue: [],
|
|
};
|
|
|
|
const github = {
|
|
currentIssue,
|
|
rest: {
|
|
issues: {
|
|
async get(payload) {
|
|
calls.getIssue.push(payload);
|
|
return { data: github.currentIssue };
|
|
},
|
|
async getLabel({ name }) {
|
|
calls.getLabel.push(name);
|
|
if (existingLabels.has(name)) {
|
|
return { data: { name } };
|
|
}
|
|
const error = new Error("Not Found");
|
|
error.status = 404;
|
|
throw error;
|
|
},
|
|
async createLabel(payload) {
|
|
calls.createLabel.push(payload);
|
|
existingLabels.add(payload.name);
|
|
return { data: payload };
|
|
},
|
|
async addLabels(payload) {
|
|
calls.addLabels.push(payload);
|
|
return { data: payload };
|
|
},
|
|
async removeLabel(payload) {
|
|
calls.removeLabel.push(payload);
|
|
return { data: payload };
|
|
},
|
|
async createComment(payload) {
|
|
calls.createComment.push(payload);
|
|
return { data: payload };
|
|
},
|
|
listForRepo: Symbol("listForRepo"),
|
|
listComments: Symbol("listComments"),
|
|
},
|
|
repos: {
|
|
async getLatestRelease() {
|
|
calls.getLatestRelease.push(true);
|
|
return { data: { tag_name: `v${latestVersion}` } };
|
|
},
|
|
},
|
|
},
|
|
async paginate(endpoint) {
|
|
calls.paginate.push(endpoint);
|
|
return endpoint === github.rest.issues.listForRepo ? issues : existingComments;
|
|
},
|
|
};
|
|
|
|
return { github, calls };
|
|
}
|
|
|
|
// Existing fixtures describe a current open report unless a test supplies a
|
|
// different live read. The production helper still makes the actual GET.
|
|
async function syncLabels(args) {
|
|
if (!args.github.currentIssue) {
|
|
args.github.currentIssue = { ...args.context.payload.issue, state: "open" };
|
|
}
|
|
return triage.syncLabels(args);
|
|
}
|
|
|
|
function createContext({ action = "opened", issue, previousBody }) {
|
|
return {
|
|
payload: {
|
|
action,
|
|
issue: { updated_at: "2026-10-08T10:00:00Z", ...issue },
|
|
...(previousBody === undefined ? {} : { changes: { body: { from: previousBody } } }),
|
|
},
|
|
repo: {
|
|
owner: "rcourtman",
|
|
repo: "Pulse",
|
|
},
|
|
};
|
|
}
|
|
|
|
function createCore() {
|
|
return {
|
|
info() {},
|
|
warning() {},
|
|
};
|
|
}
|
|
|
|
test("syncLabels classifies older bug reports without requesting a retest", async () => {
|
|
const { github, calls } = createGithub({ latestVersion: "6.0.1" });
|
|
const issue = {
|
|
number: 1402,
|
|
title: "Standalone hosts disappear after upgrade",
|
|
body: "## Feedback type\nBug / regression\n\n## Pulse version\n6.0.0-rc.1\n",
|
|
labels: [],
|
|
};
|
|
|
|
await syncLabels({
|
|
github,
|
|
context: createContext({ issue }),
|
|
core: createCore(),
|
|
});
|
|
|
|
assert.equal(calls.addLabels.length, 1);
|
|
assert.deepEqual(calls.addLabels[0].labels, [
|
|
"affects-6.0.0-rc.1",
|
|
"bug",
|
|
]);
|
|
});
|
|
|
|
test("syncLabels only adds documentation classification for non-bug v6 feedback", async () => {
|
|
const { github, calls } = createGithub({ latestVersion: "6.0.1" });
|
|
const issue = {
|
|
number: 1415,
|
|
title: "Docs path is wrong",
|
|
body: "## Feedback type\nDocumentation issue\n\n## Pulse version\n6.0.0-rc.1\n",
|
|
labels: [],
|
|
};
|
|
|
|
await syncLabels({
|
|
github,
|
|
context: createContext({ issue }),
|
|
core: createCore(),
|
|
});
|
|
|
|
assert.equal(calls.addLabels.length, 1);
|
|
assert.deepEqual(calls.addLabels[0].labels, ["documentation"]);
|
|
});
|
|
|
|
test("syncLabels marks declared secondary topics for decomposition", async () => {
|
|
const { github, calls } = createGithub({ latestVersion: "6.4.1" });
|
|
const issue = {
|
|
number: 1796,
|
|
title: "Availability workflow feedback",
|
|
body: [
|
|
"## Problem",
|
|
"Machine availability is hard to scan.",
|
|
"",
|
|
"## Additional actionable topics",
|
|
"The triage bot should preserve secondary requests.",
|
|
].join("\n"),
|
|
labels: [{ name: "enhancement" }],
|
|
};
|
|
|
|
await syncLabels({
|
|
github,
|
|
context: createContext({ issue }),
|
|
core: createCore(),
|
|
});
|
|
|
|
assert.deepEqual(calls.createLabel.map((call) => call.name), [
|
|
"needs-decomposition",
|
|
]);
|
|
assert.deepEqual(calls.addLabels[0].labels, [
|
|
"needs-decomposition",
|
|
]);
|
|
});
|
|
|
|
test("syncLabels preserves community decomposition on an empty form field", async () => {
|
|
for (const action of ["edited", "reopened"]) {
|
|
const { github, calls } = createGithub({ latestVersion: "6.4.1" });
|
|
const issue = {
|
|
number: 1796,
|
|
title: "Availability workflow feedback",
|
|
body: "## Additional actionable topics\nNone.\n",
|
|
// A later comment may have raised a distinct topic after this form was filed.
|
|
labels: [{ name: "enhancement" }, { name: "needs-decomposition" }],
|
|
};
|
|
|
|
await syncLabels({
|
|
github,
|
|
context: createContext({ action, issue }),
|
|
core: createCore(),
|
|
});
|
|
|
|
assert.equal(calls.createLabel.length, 0);
|
|
assert.equal(calls.addLabels.length, 0);
|
|
assert.deepEqual(calls.removeLabel, []);
|
|
}
|
|
});
|
|
|
|
test("syncLabels adds newly declared topics but respects a completed disposition", async () => {
|
|
const topicBody = "## Additional actionable topics\nAdd a storage filter.\n";
|
|
const emptyBody = "## Additional actionable topics\nNone.\n";
|
|
for (const { action, previousBody, expectedAdd } of [
|
|
{ action: "edited", previousBody: emptyBody, expectedAdd: true },
|
|
{ action: "edited", previousBody: topicBody, expectedAdd: false },
|
|
{ action: "edited", expectedAdd: false },
|
|
{ action: "reopened", expectedAdd: false },
|
|
]) {
|
|
const { github, calls } = createGithub();
|
|
await syncLabels({
|
|
github,
|
|
context: createContext({ action, previousBody, issue: {
|
|
number: 1796,
|
|
title: "Availability workflow feedback",
|
|
body: topicBody,
|
|
labels: [{ name: "enhancement" }],
|
|
} }),
|
|
core: createCore(),
|
|
});
|
|
assert.deepEqual(calls.addLabels.flatMap(call => call.labels),
|
|
expectedAdd ? ["needs-decomposition"] : []);
|
|
}
|
|
});
|
|
|
|
test("additional topic classification is explicit and fail-quiet for legacy forms", () => {
|
|
const { classifyAdditionalActionableTopics } = triage.internals;
|
|
|
|
assert.equal(classifyAdditionalActionableTopics("## Problem\nOne thing\n"), null);
|
|
assert.equal(
|
|
classifyAdditionalActionableTopics("## Additional actionable topics\n_No response_\n"),
|
|
false
|
|
);
|
|
assert.equal(
|
|
classifyAdditionalActionableTopics("## Additional actionable topics\nNone known.\n"),
|
|
false
|
|
);
|
|
assert.equal(
|
|
classifyAdditionalActionableTopics(
|
|
"## Additional actionable topics\n<!<!-- -->-->\n"
|
|
),
|
|
false
|
|
);
|
|
assert.equal(
|
|
classifyAdditionalActionableTopics(
|
|
"## Additional actionable topics\n- Add a storage filter\n- Reduce log noise\n"
|
|
),
|
|
true
|
|
);
|
|
assert.equal(
|
|
classifyAdditionalActionableTopics(
|
|
[
|
|
"### Additional actionable topics",
|
|
"### 2. Proxmox VE Agent Install Command",
|
|
"The generated command should expose its security controls.",
|
|
"",
|
|
"### Pulse version",
|
|
"v6.4.1",
|
|
].join("\n")
|
|
),
|
|
true
|
|
);
|
|
});
|
|
|
|
test("edited form headings accept Markdown indentation and closing hashes", () => {
|
|
const { extractPulseVersion, classifyAdditionalActionableTopics, classifyV6FeedbackType } = triage.internals;
|
|
for (const newline of ["\n", "\r\n"]) {
|
|
for (let level = 1; level <= 6; level += 1) {
|
|
for (let indent = 0; indent <= 3; indent += 1) {
|
|
for (const closing of ["", " #", " ######## \t"]) {
|
|
const heading = (name) => " ".repeat(indent) + "#".repeat(level) + "\t" + name + closing;
|
|
const body = [heading("Pulse version"), "6.5.0", "",
|
|
heading("Feedback type"), "Documentation issue", "",
|
|
heading("Additional actionable topics"), "The second host loses its Docker inventory."]
|
|
.join(newline);
|
|
assert.equal(extractPulseVersion("Upgrade from v6.4.5", body), "6.5.0", body);
|
|
assert.equal(classifyV6FeedbackType(body), "documentation", body);
|
|
assert.equal(classifyAdditionalActionableTopics(body), true, body);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
test("closing hashes and indentation do not let neighbouring fields become topics", () => {
|
|
for (const followingField of ["Pulse version", "Additional context", "Logs, screenshots, or diagnostics", "Confirmations"]) {
|
|
const body = [" ### Additional actionable topics ###", "None", "",
|
|
` ## ${followingField} ####`, "An error outside the declared topic field."].join("\n");
|
|
assert.equal(triage.internals.classifyAdditionalActionableTopics(body), false, body);
|
|
}
|
|
const body = [" ### Additional actionable topics #", "None", "",
|
|
" #### Separate backup problem ###", "Backups belong to the wrong host.", "",
|
|
" ### Pulse version ####", "6.5.0"].join("\n");
|
|
assert.equal(triage.internals.classifyAdditionalActionableTopics(body), true);
|
|
});
|
|
|
|
test("plain hash-prefixed text cannot declare issue-form metadata", () => {
|
|
const { extractPulseVersion, classifyAdditionalActionableTopics, classifyV6FeedbackType } = triage.internals;
|
|
for (const prefix of ["####### ", "###", " ### ", "\t### ", "> ### "]) {
|
|
const body = [prefix + "Pulse version", "6.5.0", "",
|
|
prefix + "Feedback type", "Bug / regression", "",
|
|
prefix + "Additional actionable topics", "Copied template text."].join("\n");
|
|
assert.equal(extractPulseVersion("No version in title", body), null, body);
|
|
assert.equal(classifyV6FeedbackType(body), null, body);
|
|
assert.equal(classifyAdditionalActionableTopics(body), null, body);
|
|
}
|
|
// A seven-hash line inside a topic is ordinary field content, not a boundary.
|
|
assert.equal(classifyAdditionalActionableTopics(
|
|
"### Additional actionable topics\nNone\n####### Pulse version\nA second symptom.\n"
|
|
), true);
|
|
});
|
|
|
|
test("only a separated final hash run closes an issue-form heading", () => {
|
|
for (const suffix of ["#", " ### example", " ## #"]) {
|
|
const body = `### Additional actionable topics${suffix}\nA second bug.\n`;
|
|
assert.equal(triage.internals.classifyAdditionalActionableTopics(body), null, body);
|
|
}
|
|
});
|
|
|
|
test("edited headings stay hidden inside comments and pasted code", () => {
|
|
const { extractPulseVersion, classifyAdditionalActionableTopics, classifyV6FeedbackType } = triage.internals;
|
|
const copied = [" ### Pulse version ###", "6.4.1", " ### Feedback type ###",
|
|
"Bug / regression", " ### Additional actionable topics ###", "Copied example."].join("\n");
|
|
for (const [open, close] of [["<!--", "-->"], ["```markdown", "```"], ["~~~markdown", "~~~"]]) {
|
|
const body = [open, copied, close, " ### Pulse version ###", "unknown",
|
|
" ### Feedback type ###", "Documentation issue", " ### Additional actionable topics ###", "None"]
|
|
.join("\n");
|
|
assert.equal(extractPulseVersion("Upgrade from v6.4.5", body), null, body);
|
|
assert.equal(classifyV6FeedbackType(body), "documentation", body);
|
|
assert.equal(classifyAdditionalActionableTopics(body), false, body);
|
|
}
|
|
});
|
|
|
|
test("edited version headings retain fenced values and authoritative unknowns", () => {
|
|
for (const value of ["6.5.0-rc.1", "unknown", "6.5"]) {
|
|
const body = [" ### Pulse version ###", "```text", value, "```", "",
|
|
" ### Agent version ###", "6.4.5"].join("\n");
|
|
assert.equal(triage.internals.extractPulseVersion("Upgrade from v6.4.5", body),
|
|
value === "6.5.0-rc.1" ? value : null, body);
|
|
}
|
|
});
|
|
|
|
test("label consumer uses edited headings without changing Community-owned state", async () => {
|
|
const { github, calls } = createGithub();
|
|
const issue = { number: 2900, title: "Upgrade from v6.4.5",
|
|
body: " ### Feedback type ###\nBug / regression\n ### Pulse version ###\n6.5.0\n" +
|
|
" ### Additional actionable topics ###\nThe second host loses Docker inventory.\n",
|
|
labels: ["enhancement", "needs-retest-on-latest", "needs-human", "operator-reviewed", "affects-6.4.5"]
|
|
.map(name => ({ name })) };
|
|
await syncLabels({ github, core: createCore(), context: createContext({ issue }) });
|
|
assert.deepEqual(calls.addLabels.map(call => call.labels),
|
|
[["affects-6.5.0", "bug", "needs-decomposition"]]);
|
|
assert.deepEqual(calls.removeLabel.map(call => call.name), ["affects-6.4.5"]);
|
|
assert.deepEqual(calls.createComment, []);
|
|
assert.deepEqual(calls.getLatestRelease, []);
|
|
assert.deepEqual(calls.paginate, []);
|
|
assert.deepEqual(calls.getIssue.map(call => call.issue_number), [2900, 2900]);
|
|
});
|
|
|
|
test("invalid headings cannot create classification or decomposition mutations", async () => {
|
|
const { github, calls } = createGithub();
|
|
const issue = { number: 2901, title: "An example in a documentation report",
|
|
body: "####### Feedback type\nBug / regression\n####### Pulse version\n6.5.0\n" +
|
|
"####### Additional actionable topics\nCopied template text.\n",
|
|
labels: ["documentation", "needs-retest-on-latest", "needs-human", "operator-reviewed"]
|
|
.map(name => ({ name })) };
|
|
await syncLabels({ github, core: createCore(), context: createContext({ issue }) });
|
|
assert.deepEqual(calls.addLabels, []);
|
|
assert.deepEqual(calls.removeLabel, []);
|
|
assert.deepEqual(calls.createLabel, []);
|
|
assert.deepEqual(calls.createComment, []);
|
|
assert.deepEqual(calls.getLatestRelease, []);
|
|
assert.deepEqual(calls.paginate, []);
|
|
});
|
|
|
|
test("topic classification reads later paragraphs after an empty first line", () => {
|
|
for (const firstLine of ["None", "<!-- List distinct topics here. -->", "_No response_"]) {
|
|
const body = [
|
|
"### Additional actionable topics", firstLine, "",
|
|
"The second host also loses its Docker inventory.", "",
|
|
"### Pulse version", "6.5.0",
|
|
].join("\n");
|
|
assert.equal(triage.internals.classifyAdditionalActionableTopics(body), true);
|
|
}
|
|
});
|
|
|
|
test("topic classification stops at form fields but retains topic subheadings", () => {
|
|
const { classifyAdditionalActionableTopics } = triage.internals;
|
|
for (const newline of ["\n", "\r\n"]) {
|
|
for (const followingField of ["Pulse version", "Additional context", "Logs, screenshots, or diagnostics", "Confirmations"]) {
|
|
assert.equal(classifyAdditionalActionableTopics([
|
|
"### Additional actionable topics", "None", "",
|
|
`### ${followingField}`, "An error or a second topic outside this field.",
|
|
].join(newline)), false);
|
|
}
|
|
assert.equal(classifyAdditionalActionableTopics([
|
|
"### Additional actionable topics", "None", "",
|
|
"### Separate backup problem", "Backups belong to the wrong host.",
|
|
"### Pulse version", "6.5.0",
|
|
].join(newline)), true);
|
|
}
|
|
});
|
|
|
|
test("hidden template fields cannot declare topics or override feedback type", () => {
|
|
const { classifyAdditionalActionableTopics, classifyV6FeedbackType } = triage.internals;
|
|
const hidden = ["<!--", "### Additional actionable topics", "A second bug.",
|
|
"### Feedback type", "Bug / regression", "-->"].join("\n");
|
|
assert.equal(classifyAdditionalActionableTopics(hidden), null);
|
|
assert.equal(classifyV6FeedbackType(hidden), null);
|
|
assert.equal(classifyAdditionalActionableTopics(
|
|
`${hidden}\n### Additional actionable topics\nNone\n\n### Pulse version\n6.5.0`
|
|
), false);
|
|
assert.equal(classifyV6FeedbackType(
|
|
`${hidden}\n### Feedback type\nDocumentation issue\n\n### Pulse version\n6.5.0`
|
|
), "documentation");
|
|
assert.equal(classifyV6FeedbackType(
|
|
"### Feedback type\nDocumentation issue\n\n### Describe the bug\nBug / regression"
|
|
), "documentation");
|
|
});
|
|
|
|
test("literal HTML comments in pasted code cannot erase later visible form fields", () => {
|
|
const { extractPulseVersion, classifyAdditionalActionableTopics, classifyV6FeedbackType } = triage.internals;
|
|
for (const newline of ["\n", "\r\n"]) {
|
|
for (const [open, close] of [["```html", "```"], ["~~~~html", "~~~~"],
|
|
[" ````text", " ````"]]) {
|
|
for (const marker of ["<!-- unfinished response", "<!-- closed response -->", "<!<!-- -->-->"]) {
|
|
const body = [open, marker, "### Pulse version", "6.4.1", close, "",
|
|
"### Pulse version", "6.5.0", "### Feedback type", "Documentation issue",
|
|
"### Additional actionable topics", "Missing disk reading"].join(newline);
|
|
assert.equal(extractPulseVersion("Upgrade from v6.4.5", body), "6.5.0", body);
|
|
assert.equal(classifyV6FeedbackType(body), "documentation", body);
|
|
assert.equal(classifyAdditionalActionableTopics(body), true, body);
|
|
}
|
|
}
|
|
for (const indent of [" ", "\t"]) {
|
|
const body = [`${indent}<!-- copied log delimiter`, `${indent}Pulse version: 6.4.1`, "",
|
|
"### Pulse version", "6.5.0", "### Feedback type", "Documentation issue",
|
|
"### Additional actionable topics", "Missing disk reading"].join(newline);
|
|
assert.equal(extractPulseVersion("Upgrade from v6.4.5", body), "6.5.0", body);
|
|
assert.equal(classifyV6FeedbackType(body), "documentation", body);
|
|
assert.equal(classifyAdditionalActionableTopics(body), true, body);
|
|
}
|
|
}
|
|
});
|
|
|
|
test("hidden comments still hide their contents even when they contain code fences", () => {
|
|
const { extractPulseVersion, classifyAdditionalActionableTopics, classifyV6FeedbackType } = triage.internals;
|
|
const hidden = ["<!-- hidden template", "```markdown", "### Pulse version", "6.4.1",
|
|
"### Feedback type", "Bug / regression", "### Additional actionable topics", "Copied topic", "```"];
|
|
const visible = ["### Pulse version", "6.5.0", "### Feedback type", "Documentation issue",
|
|
"### Additional actionable topics", "None"];
|
|
for (const newline of ["\n", "\r\n"]) {
|
|
const closed = [...hidden, " -->", ...visible].join(newline);
|
|
assert.equal(extractPulseVersion("Upgrade from v6.4.5", closed), "6.5.0");
|
|
assert.equal(classifyV6FeedbackType(closed), "documentation");
|
|
assert.equal(classifyAdditionalActionableTopics(closed), false);
|
|
const unfinished = [...hidden, ...visible].join(newline);
|
|
assert.equal(extractPulseVersion("Bug", unfinished), null);
|
|
assert.equal(classifyV6FeedbackType(unfinished), null);
|
|
assert.equal(classifyAdditionalActionableTopics(unfinished), null);
|
|
}
|
|
});
|
|
|
|
test("pasted HTML keeps genuine topic values without turning copied headings into fields", () => {
|
|
const body = ["### Additional actionable topics", "```html", "<!-- unmatched response marker",
|
|
"### Pulse version", "6.4.1", "```", "### Pulse version", "6.5.0"].join("\n");
|
|
assert.equal(triage.internals.classifyAdditionalActionableTopics(body), true);
|
|
assert.equal(triage.internals.extractPulseVersion("Upgrade from v6.4.5", body), "6.5.0");
|
|
});
|
|
|
|
test("inline and quoted HTML literals cannot hide subsequent form declarations", () => {
|
|
const { extractPulseVersion, classifyV6FeedbackType, classifyAdditionalActionableTopics } = triage.internals;
|
|
for (const evidence of [
|
|
"The error includes `<!-- unfinished`.",
|
|
"The error includes ``a ` tick and <!-- unfinished``.",
|
|
"The error includes `<!-- first` and ``<!-- second``.",
|
|
"> ```html\n> <!-- unfinished\n> ### Pulse version\n> 6.4.1\n> ```",
|
|
" > The response includes <!-- unfinished",
|
|
"The literal is \\<!-- unfinished.",
|
|
]) {
|
|
for (const newline of ["\n", "\r\n"]) {
|
|
const body = ["### Existing evidence", evidence, "", "### Pulse version", "6.5.0",
|
|
"### Feedback type", "Documentation issue", "### Additional actionable topics",
|
|
"Storage History remains empty."].join("\n").replaceAll("\n", newline);
|
|
assert.equal(extractPulseVersion("Upgrade from v6.4.5", body), "6.5.0", body);
|
|
assert.equal(classifyV6FeedbackType(body), "documentation", body);
|
|
assert.equal(classifyAdditionalActionableTopics(body), true, body);
|
|
}
|
|
}
|
|
});
|
|
|
|
test("an interrupted inline comment cannot turn an unclosed code fence into visible fields", () => {
|
|
const { extractPulseVersion, classifyV6FeedbackType, classifyAdditionalActionableTopics } = triage.internals;
|
|
for (const evidence of [
|
|
"A literal `<!-- unfinished` followed by <!--",
|
|
"A mismatched `<!-- opener`` remains text before a block quote",
|
|
"An escaped \\` backtick does not protect <!-- unfinished`",
|
|
"An even escape \\\\<!-- remains text before the quote",
|
|
"<!-- a hidden ` backtick is not an opener --> <!-- second hidden`",
|
|
]) {
|
|
const body = [evidence, "> quoted text", "```", "### Pulse version", "6.4.1",
|
|
"### Feedback type", "Bug / regression", "### Additional actionable topics", "Copied topic",
|
|
"-->", "### Pulse version", "unknown", "### Feedback type", "Documentation issue",
|
|
"### Additional actionable topics", "None"].join("\n");
|
|
// GFM's block lexer puts every later line inside the unclosed fence.
|
|
// The previous scanner mistakenly treated an inline opener as a comment
|
|
// across those blocks, then accepted invisible headings after its close.
|
|
assert.equal(extractPulseVersion("Upgrade from v6.4.5", body), null, body);
|
|
assert.equal(classifyV6FeedbackType(body), null, body);
|
|
assert.equal(classifyAdditionalActionableTopics(body), null, body);
|
|
const visible = body + "\n```\n### Pulse version\n6.5.0\n" +
|
|
"### Feedback type\nDocumentation issue\n### Additional actionable topics\nNone";
|
|
assert.equal(extractPulseVersion("Upgrade from v6.4.5", visible), "6.5.0", visible);
|
|
assert.equal(classifyV6FeedbackType(visible), "documentation", visible);
|
|
assert.equal(classifyAdditionalActionableTopics(visible), false, visible);
|
|
}
|
|
});
|
|
|
|
test("syncLabels preserves visible fields after inline HTML without new contact", async () => {
|
|
for (const evidence of ["Error: `<!-- unfinished`", "> <!-- unfinished response"]) {
|
|
const { github, calls } = createGithub();
|
|
const issue = { number: 2808, title: "Upgrade from v6.4.5",
|
|
body: ["### Existing evidence", evidence, "", "### Pulse version", "6.5.0",
|
|
"### Feedback type", "Bug / regression", "### Additional actionable topics", "Missing History"].join("\n"),
|
|
labels: ["needs-human", "needs-retest-on-latest", "operator-reviewed", "needs-version-info"]
|
|
.map(name => ({ name })) };
|
|
await syncLabels({ github, core: createCore(), context: createContext({ issue }) });
|
|
assert.deepEqual(calls.addLabels.flatMap(call => call.labels), ["affects-6.5.0", "bug", "needs-decomposition"]);
|
|
assert.deepEqual(calls.removeLabel.map(call => call.name), ["needs-version-info"]);
|
|
assert.deepEqual(calls.createComment, []);
|
|
assert.deepEqual(calls.getLatestRelease, []);
|
|
assert.deepEqual(calls.paginate, []);
|
|
assert.deepEqual(calls.getIssue.map(call => call.issue_number), [2808, 2808]);
|
|
}
|
|
});
|
|
|
|
test("syncLabels consumes real fields after pasted HTML without contact or state changes", async () => {
|
|
const { github, calls } = createGithub();
|
|
const issue = { number: 2801, title: "Upgrade from v6.4.5",
|
|
body: ["```html", "<!-- incomplete response", "### Pulse version", "6.4.1", "```",
|
|
"### Pulse version", "6.5.0", "### Feedback type", "Bug / regression",
|
|
"### Additional actionable topics", "Missing disk reading"].join("\n"),
|
|
labels: ["needs-human", "needs-retest-on-latest", "operator-reviewed", "needs-version-info"]
|
|
.map(name => ({ name })) };
|
|
await syncLabels({ github, core: createCore(), context: createContext({ issue }) });
|
|
assert.deepEqual(calls.addLabels.flatMap(call => call.labels), ["affects-6.5.0", "bug", "needs-decomposition"]);
|
|
assert.deepEqual(calls.removeLabel.map(call => call.name), ["needs-version-info"]);
|
|
assert.deepEqual(calls.createComment, []);
|
|
assert.deepEqual(calls.getLatestRelease, []);
|
|
assert.deepEqual(calls.paginate, []);
|
|
assert.deepEqual(calls.getIssue.map(call => call.issue_number), [2801, 2801]);
|
|
});
|
|
|
|
test("a hidden hint or empty topic field is not an actionable declaration", () => {
|
|
for (const value of ["", "<!-- A second bug. -->", "None\n\n<!-- Another topic. -->"]) {
|
|
assert.equal(triage.internals.classifyAdditionalActionableTopics(
|
|
`### Additional actionable topics\n${value}\n\n### Pulse version\n6.5.0`
|
|
), false);
|
|
}
|
|
});
|
|
|
|
test("syncLabels surfaces newly added later-line topics without resetting Community state", async () => {
|
|
const previousBody = "### Additional actionable topics\nNone\n\n### Pulse version\n6.5.0";
|
|
const body = "### Additional actionable topics\nNone\n\n" +
|
|
"Backup attribution is also wrong.\n\n### Pulse version\n6.5.0";
|
|
for (const action of ["opened", "edited", "reopened"]) {
|
|
const { github, calls } = createGithub();
|
|
const issue = { number: 2800, title: "Host inventory report", body,
|
|
labels: [{ name: "enhancement" }, { name: "needs-retest-on-latest" }, { name: "needs-human" }] };
|
|
await syncLabels({ github, core: createCore(),
|
|
context: createContext({ action, issue, previousBody }) });
|
|
assert.deepEqual(calls.addLabels.flatMap(call => call.labels),
|
|
action === "reopened" ? [] : ["needs-decomposition"]);
|
|
assert.deepEqual(calls.removeLabel, []);
|
|
assert.deepEqual(calls.createComment, []);
|
|
assert.deepEqual(calls.getLatestRelease, []);
|
|
assert.deepEqual(calls.paginate, []);
|
|
assert.deepEqual(calls.getIssue.map(call => call.issue_number), action === "reopened" ? [2800] : [2800, 2800]);
|
|
}
|
|
});
|
|
|
|
test("syncLabels ignores hidden bug examples and topic hints", async () => {
|
|
const { github, calls } = createGithub();
|
|
const issue = { number: 2801, title: "Documentation wording",
|
|
body: "<!--\n### Feedback type\nBug / regression\n" +
|
|
"### Additional actionable topics\nAnother bug.\n-->\n" +
|
|
"### Feedback type\nDocumentation issue\n\n" +
|
|
"### Additional actionable topics\nNone\n\n### Pulse version\n6.5.0",
|
|
labels: [{ name: "needs-retest-on-latest" }, { name: "needs-decomposition" }] };
|
|
await syncLabels({ github, core: createCore(), context: createContext({ issue }) });
|
|
assert.deepEqual(calls.addLabels.flatMap(call => call.labels), ["documentation"]);
|
|
assert.deepEqual(calls.createLabel.map(call => call.name), []);
|
|
assert.deepEqual(calls.removeLabel, []);
|
|
assert.deepEqual(calls.createComment, []);
|
|
assert.deepEqual(calls.getLatestRelease, []);
|
|
assert.deepEqual(calls.paginate, []);
|
|
});
|
|
|
|
test("fenced examples cannot declare issue metadata", () => {
|
|
const { extractPulseVersion, classifyAdditionalActionableTopics, classifyV6FeedbackType } = triage.internals;
|
|
for (const fence of ["```", "~~~~", " ````"]) {
|
|
for (const newline of ["\n", "\r\n"]) {
|
|
const body = ["### Existing evidence", `${fence}markdown`,
|
|
"### Pulse version", "6.4.1", "### Feedback type", "Bug / regression",
|
|
"### Additional actionable topics", "A copied example, not a new topic.",
|
|
fence].join(newline);
|
|
assert.equal(extractPulseVersion("No declared version", body), null, body);
|
|
assert.equal(extractPulseVersion("Bug on v6.5.0", body), "6.5.0", body);
|
|
assert.equal(classifyV6FeedbackType(body), null, body);
|
|
assert.equal(classifyAdditionalActionableTopics(body), null, body);
|
|
}
|
|
}
|
|
});
|
|
|
|
test("only matching complete fences end pasted examples", () => {
|
|
const { extractPulseVersion, classifyV6FeedbackType } = triage.internals;
|
|
for (const { open, falseClose } of [
|
|
{ open: "````text", falseClose: "```" },
|
|
{ open: "```text", falseClose: "~~~" },
|
|
{ open: "~~~text", falseClose: "```" },
|
|
{ open: "```text", falseClose: "``` still part of the log" },
|
|
{ open: "```text", falseClose: "" },
|
|
]) {
|
|
const body = [open, "Log text", falseClose, "### Pulse version", "6.4.1",
|
|
"### Feedback type", "Regression"].join("\n");
|
|
assert.equal(extractPulseVersion("Unknown", body), null, body);
|
|
assert.equal(classifyV6FeedbackType(body), null, body);
|
|
}
|
|
});
|
|
|
|
test("real declarations after fenced examples remain authoritative", () => {
|
|
const { extractPulseVersion, classifyAdditionalActionableTopics, classifyV6FeedbackType } = triage.internals;
|
|
for (const fence of ["```", "~~~"]) {
|
|
const body = [fence, "Pulse version: 6.4.1", "### Pulse version", "6.4.1",
|
|
"### Feedback type", "Regression", "### Additional actionable topics", "Copied topic", fence,
|
|
"### Feedback type", "Documentation issue", "### Additional actionable topics", "None",
|
|
"### Pulse version", "6.5.0"].join("\n");
|
|
assert.equal(extractPulseVersion("Upgrade from v6.4.5", body), "6.5.0", body);
|
|
assert.equal(classifyV6FeedbackType(body), "documentation", body);
|
|
assert.equal(classifyAdditionalActionableTopics(body), false, body);
|
|
}
|
|
});
|
|
|
|
test("indented log fields cannot declare a legacy version", () => {
|
|
for (const indent of [" ", "\t"]) {
|
|
const body = `${indent}Pulse version: 6.4.1\n${indent}Agent version: 6.4.1\n`;
|
|
assert.equal(triage.internals.extractPulseVersion("No declared version", body), null, body);
|
|
assert.equal(triage.internals.extractPulseVersion("Bug on v6.5.0", body), "6.5.0", body);
|
|
}
|
|
});
|
|
|
|
test("real version fields preserve fenced values without borrowing later evidence", () => {
|
|
const { extractPulseVersion } = triage.internals;
|
|
for (const field of ["### Pulse version", "Pulse | Version"]) {
|
|
for (const fence of ["````", "~~~"]) {
|
|
assert.equal(extractPulseVersion("Upgrade from v6.4.5", [field, `${fence}text`,
|
|
"V6.5.0-rc.1", fence, "### Agent version", "6.4.5"].join("\n")), "6.5.0-rc.1");
|
|
assert.equal(extractPulseVersion("Upgrade from v6.4.5", [field, "unknown", `${fence}text`,
|
|
"Agent version: 6.5.0", fence].join("\n")), null);
|
|
assert.equal(extractPulseVersion("Upgrade from v6.4.5", [field, "6.4", `${fence}text`,
|
|
"6.5.0", fence].join("\n")), null);
|
|
}
|
|
}
|
|
});
|
|
|
|
test("structured product-named versions remain declared evidence", () => {
|
|
assert.equal(triage.internals.extractPulseVersion("Bug",
|
|
"### Pulse version\nPulse v6.4.5-beta.1\n### Agent version\n6.4.5"), "6.4.5-beta.1");
|
|
assert.equal(triage.internals.extractPulseVersion("Bug",
|
|
"### Pulse version\nConfirmed on a modified main build around v6.4.5-rc.2\n"), null);
|
|
});
|
|
|
|
test("fenced headings do not truncate genuine secondary topics", () => {
|
|
const body = ["### Additional actionable topics", "The generated command also needs review:",
|
|
"```markdown", "### Pulse version", "A heading in the example, not a new field.", "```",
|
|
"The command must preserve both mounts.", "### Pulse version", "6.5.0"].join("\n");
|
|
assert.equal(triage.internals.classifyAdditionalActionableTopics(body), true);
|
|
});
|
|
|
|
test("feedback keeps its selected value separate from fenced examples", () => {
|
|
const body = ["### Feedback type", "Documentation issue", "```text", "Regression example",
|
|
"### Pulse version", "6.4.1", "```", "### Pulse version", "6.5.0"].join("\n");
|
|
assert.equal(triage.internals.classifyV6FeedbackType(body), "documentation");
|
|
assert.equal(triage.internals.extractPulseVersion("Bug", body), "6.5.0");
|
|
});
|
|
|
|
test("syncLabels cannot classify copied issue templates or disturb Community state", async () => {
|
|
const { github, calls } = createGithub();
|
|
const issue = { number: 2806, title: "Documentation wording",
|
|
body: "```markdown\n### Feedback type\nBug / regression\n### Pulse version\n6.4.1\n" +
|
|
"### Additional actionable topics\nCopied example\n```\n",
|
|
labels: ["enhancement", "needs-retest-on-latest", "needs-human", "needs-decomposition"]
|
|
.map(name => ({ name })) };
|
|
await syncLabels({ github, core: createCore(), context: createContext({ issue }) });
|
|
assert.deepEqual(calls.addLabels, []);
|
|
assert.deepEqual(calls.removeLabel, []);
|
|
assert.deepEqual(calls.createLabel, []);
|
|
assert.deepEqual(calls.createComment, []);
|
|
assert.deepEqual(calls.getLatestRelease, []);
|
|
assert.deepEqual(calls.paginate, []);
|
|
assert.deepEqual(calls.getIssue.map(call => call.issue_number), [2806]);
|
|
});
|
|
|
|
test("syncLabels keeps structured unknown separate from a quoted agent version", async () => {
|
|
const { github, calls } = createGithub();
|
|
const issue = { number: 2807, title: "Upgrade from v6.4.5",
|
|
body: "### Pulse version\nunknown\n```text\nAgent version: 6.5.0\n```\n",
|
|
labels: ["bug", "needs-retest-on-latest", "needs-decomposition", "operator-reviewed"]
|
|
.map(name => ({ name })) };
|
|
await syncLabels({ github, core: createCore(), context: createContext({ issue }) });
|
|
assert.deepEqual(calls.addLabels.map(call => call.labels), [["needs-version-info"]]);
|
|
assert.deepEqual(calls.createLabel.map(call => call.name), ["needs-version-info"]);
|
|
assert.deepEqual(calls.removeLabel, []);
|
|
assert.deepEqual(calls.createComment, []);
|
|
assert.deepEqual(calls.getLatestRelease, []);
|
|
});
|
|
|
|
test("every actionable issue form exposes the decomposition signal", () => {
|
|
const templateDir = path.resolve(__dirname, "../ISSUE_TEMPLATE");
|
|
for (const name of [
|
|
"bug_report.yml",
|
|
"feature_request.yml",
|
|
"v6_rc_feedback.yml",
|
|
]) {
|
|
const form = fs.readFileSync(path.join(templateDir, name), "utf8");
|
|
assert.match(form, /id: additional_topics/);
|
|
assert.match(form, /label: Additional actionable topics/);
|
|
assert.match(
|
|
form,
|
|
/id: additional_topics[\s\S]*?validations:\s*\n\s+required: true/
|
|
);
|
|
}
|
|
});
|
|
|
|
test("bug and pre-release forms accept truthful evidence for installs that never started", () => {
|
|
const templateDir = path.resolve(__dirname, "../ISSUE_TEMPLATE");
|
|
for (const name of ["bug_report.yml", "v6_rc_feedback.yml"]) {
|
|
const form = fs.readFileSync(path.join(templateDir, name), "utf8");
|
|
assert.match(form, /id: pulse_version[\s\S]*?failed install[\s\S]*?"unknown"/);
|
|
assert.match(form, /id: installer_source[\s\S]*?Installer or helper source[\s\S]*?required: false/);
|
|
assert.match(form, /Do not paste a command containing a token or other secret/);
|
|
assert.match(form, /for an install that never started/);
|
|
}
|
|
});
|
|
|
|
test("both bug forms keep an optional failed-upgrade timeline without unsafe collection", () => {
|
|
for (const name of ["bug_report.yml", "v6_rc_feedback.yml"]) {
|
|
const form = fs.readFileSync(path.resolve(__dirname, "../ISSUE_TEMPLATE", name), "utf8");
|
|
const timeline = fieldGuidance(form, "upgrade_context");
|
|
assert.ok(timeline, name);
|
|
assert.match(timeline, /label: Upgrade attempt and recovery/);
|
|
assert.match(timeline, /required: false/);
|
|
for (const distinction of [
|
|
"starting version, intended version or asset", "original time and timezone",
|
|
"last recorded updater/installer result or exit code",
|
|
"whole LXC, VM or host stopping", "recovery you already performed",
|
|
"A verified download signature is not a completed upgrade",
|
|
"a later boot does not explain the stop",
|
|
"Today's running version does not establish the version at failure",
|
|
'blank or "unknown" is valid', "not full journals",
|
|
"Do not rerun the update, reboot, restore, run Diagnostics or delete rollback backups",
|
|
"Keep backups private and preserve the original evidence",
|
|
]) {
|
|
assert.ok(timeline.includes(distinction), `${name}: ${distinction}`);
|
|
}
|
|
const version = form.split(" id: pulse_version\n")[1].split(" - type: ")[0];
|
|
assert.match(version, /Version running when the problem occurred, or "unknown"/);
|
|
assert.match(version, /target and later recovered version separate/);
|
|
}
|
|
});
|
|
|
|
test("bug and pre-release forms accept unsafe one-off failures without a second run", () => {
|
|
const templateDir = path.resolve(__dirname, "../ISSUE_TEMPLATE");
|
|
for (const name of ["bug_report.yml", "v6_rc_feedback.yml"]) {
|
|
const form = fs.readFileSync(path.join(templateDir, name), "utf8");
|
|
assert.match(form, /do not repeat/i);
|
|
assert.match(form, /original sequence/i);
|
|
assert.match(form, /second reproduction is not required/);
|
|
assert.match(form, /data loss, an outage, duplicate changes, or excessive notifications/);
|
|
}
|
|
});
|
|
|
|
test("pre-release evidence keeps screenshots visible and asks only running containers for image identity", () => {
|
|
const form = fs.readFileSync(
|
|
path.resolve(__dirname, "../ISSUE_TEMPLATE/v6_rc_feedback.yml"),
|
|
"utf8"
|
|
);
|
|
assert.match(form, /id: image_ref[\s\S]*?For a running Docker, Compose, or Kubernetes container/);
|
|
assert.match(form, /id: image_ref[\s\S]*?Leave blank for LXC, bare metal, and failed installs/);
|
|
assert.match(form, /for a running container I also gave its Pulse image tag or digest/);
|
|
const evidenceField = form.split(" id: evidence\n")[1].split(" - type: checkboxes\n")[0];
|
|
assert.match(evidenceField, /Attach screenshots or paste relevant redacted logs/);
|
|
assert.doesNotMatch(evidenceField, /render:/);
|
|
});
|
|
|
|
for (const name of ["bug_report.yml", "v6_rc_feedback.yml"]) {
|
|
test(`report intake collection safety: ${name}`, () => {
|
|
const form = fs.readFileSync(path.resolve(__dirname, "../ISSUE_TEMPLATE", name), "utf8");
|
|
const introduction = form.split(" - type: markdown\n")[1].split(" - type: ")[0];
|
|
assert.match(introduction, /Run Diagnostics.*live API and guest-agent requests/);
|
|
assert.match(introduction, /do not run it during backups, freeze\/thaw or an unresponsive-host incident/);
|
|
assert.match(introduction, /Prefer existing observations/);
|
|
assert.match(introduction, /downloads that result without running checks again/);
|
|
assert.doesNotMatch(introduction, /Export for GitHub \(sanitized\)/);
|
|
});
|
|
}
|
|
|
|
for (const name of ["bug_report.yml", "v6_rc_feedback.yml"]) {
|
|
test(`report intake excludes unreleased branch-tip guidance: ${name}`, () => {
|
|
const form = fs.readFileSync(path.resolve(__dirname, "../ISSUE_TEMPLATE", name), "utf8");
|
|
// Safety instructions must remain self-contained for installed versions.
|
|
// A main/master link can describe runtime behaviour they do not ship.
|
|
assert.doesNotMatch(form, /https:\/\/github\.com\/[^/\s)]+\/[^/\s)]+\/(?:blob|tree)\/(?:main|master)\//);
|
|
assert.match(form, /only collect diagnostics if Pulse is running and collection is safe/);
|
|
assert.match(form, /downloads that result without running checks again/);
|
|
assert.match(form, /Review files and screenshots locally before posting/);
|
|
});
|
|
}
|
|
|
|
for (const [name, field] of [
|
|
["bug_report.yml", "logs"],
|
|
["v6_rc_feedback.yml", "evidence"],
|
|
["feature_request.yml", "additional_context"],
|
|
]) {
|
|
test(`report intake privacy at attachment point: ${name}`, () => {
|
|
const form = fs.readFileSync(path.resolve(__dirname, "../ISSUE_TEMPLATE", name), "utf8");
|
|
const introduction = form.split(" - type: markdown\n")[1].split(" - type: ")[0];
|
|
const attachment = form.split(` id: ${field}\n`)[1].split(" - type: ")[0];
|
|
assert.match(introduction, /Anything you attach here is public/);
|
|
assert.match(introduction, /even when an export is labelled "sanitized"/);
|
|
for (const sensitive of ["session cookies", "secret URLs", "private host", "personal details", "errors", ".env", "private keys", "Copy as cURL", "full network exports"]) {
|
|
assert.ok(introduction.includes(sensitive), `${name} must warn about ${sensitive}`);
|
|
}
|
|
assert.match(attachment, /Review files and screenshots locally/);
|
|
assert.match(attachment, /identifying details.*errors/);
|
|
assert.doesNotMatch(attachment, /render:/);
|
|
assert.doesNotMatch(attachment, /required: true/);
|
|
});
|
|
}
|
|
|
|
for (const [name, field] of [
|
|
["bug_report.yml", "logs"],
|
|
["v6_rc_feedback.yml", "evidence"],
|
|
]) {
|
|
const evidenceField = () => {
|
|
const form = fs.readFileSync(path.resolve(__dirname, "../ISSUE_TEMPLATE", name), "utf8");
|
|
return fieldGuidance(form, field);
|
|
};
|
|
|
|
test(`performance report measurement context stays optional: ${name}`, () => {
|
|
const attachment = evidenceField();
|
|
assert.match(attachment, /For CPU, memory or disk-write reports/);
|
|
assert.match(attachment, /existing readings or safe, passive observations/);
|
|
assert.match(attachment, /Pulse process, its container or the whole host/);
|
|
for (const context of ["units", "measurement window", "uptime", "allocated CPUs", "memory limit", "fleet size", "polling interval", "open dashboards"]) {
|
|
assert.ok(attachment.includes(context), `${name} must distinguish ${context}`);
|
|
}
|
|
assert.match(attachment, /where relevant and known/);
|
|
assert.match(attachment, /process-start CPU average is not a recent sampling window/);
|
|
assert.match(attachment, /database size is not a write rate/);
|
|
assert.match(attachment, /Existing screenshots are useful/);
|
|
assert.match(attachment, /write "unavailable" when safe collection is not possible/);
|
|
assert.doesNotMatch(attachment, /required: true|render:/);
|
|
});
|
|
|
|
test(`performance report attachment safety: ${name}`, () => {
|
|
const attachment = evidenceField();
|
|
assert.match(attachment, /Do not restart, create load or change polling or retention just to measure/);
|
|
assert.match(attachment, /Do not attach raw profiles, heap dumps, databases or full process command lines/);
|
|
assert.match(attachment, /summarise only relevant counters after local review/);
|
|
});
|
|
|
|
test(`baseline memory regressions need no continuing growth or restart: ${name}`, () => {
|
|
const attachment = evidenceField();
|
|
assert.match(attachment, /higher steady baseline from growth during one uninterrupted run/);
|
|
assert.match(attachment, /worth reporting even if usage stays flat/);
|
|
assert.match(attachment, /already available versions, uptime, workload and enabled collectors\/features/);
|
|
assert.match(attachment, /not proof of recovery compared with a days-old run/);
|
|
assert.match(attachment, /Unknown context is valid/);
|
|
assert.match(attachment, /do not restart, downgrade or change features to create a comparison/);
|
|
assert.doesNotMatch(attachment, /required: true|render:/);
|
|
});
|
|
|
|
test(`guest memory comparisons retain optional target and sample context: ${name}`, () => {
|
|
const attachment = evidenceField();
|
|
assert.match(attachment, /For CPU, memory or disk-write reports about Pulse itself/);
|
|
const guest = attachment.split("For VM or LXC memory disagreements,")[1];
|
|
assert.ok(guest, "guest readings must not inherit Pulse process-only guidance");
|
|
assert.match(guest, /overview, History, alert, Patrol or another tool/);
|
|
assert.match(guest, /inside the affected guest or on its hypervisor node/);
|
|
assert.match(guest, /original times, units and memory source or sample age only if already known or displayed/);
|
|
assert.match(guest, /consistent private aliases.*"unknown" for missing details/);
|
|
assert.match(guest, /Available memory, buff\/cache and a process's RSS measure different things/);
|
|
assert.match(guest, /RSS alone is not total guest usage/);
|
|
assert.match(guest, /cache-inclusive hypervisor footprint does not establish guest pressure/);
|
|
assert.match(guest, /A Proxmox VM API connection may still obtain QEMU guest-agent \(QGA\) readings without a Pulse agent/);
|
|
assert.doesNotMatch(attachment, /required: true|render:/);
|
|
});
|
|
|
|
test(`guest memory context does not ask for new collection or workload changes: ${name}`, () => {
|
|
const guest = evidenceField().split("For VM or LXC memory disagreements,")[1];
|
|
assert.ok(guest, "a guest comparison needs its own collection boundary");
|
|
assert.match(guest, /Use existing observations only/);
|
|
assert.match(guest, /Do not run commands, Diagnostics or guest-agent probes, install an agent, change memory settings or workloads, or repeat a backup just to answer/);
|
|
});
|
|
}
|
|
|
|
test("guest memory triage preserves original evidence rather than inferring past provenance", () => {
|
|
const guide = fs.readFileSync(path.resolve(__dirname, "../../docs/ISSUE_TRIAGE.md"), "utf8").replace(/\s+/g, " ");
|
|
const guest = guide.split("For VM or LXC memory disagreements,")[1].split("For notification reports,")[0];
|
|
assert.match(guest, /same target and episode/);
|
|
assert.match(guest, /inside the affected guest or on its hypervisor node/);
|
|
assert.match(guest, /original times, units and selected memory source or sample age if already known/);
|
|
assert.match(guest, /unknown is valid, and existing reports need no refile/);
|
|
assert.match(guest, /RSS alone is not total guest usage/);
|
|
assert.match(guest, /A Proxmox VM API connection may still obtain QEMU guest-agent \(QGA\) readings without a Pulse agent/);
|
|
assert.match(guest, /not measured zero or verified recovery/);
|
|
assert.match(guest, /live memory source cannot establish the source of a historical sample/);
|
|
assert.match(guest, /Reconcile the full thread before asking only for a consequential remaining distinction/);
|
|
assert.match(guest, /Do not request commands, Diagnostics or guest-agent probes, an agent install, memory or workload changes, or another backup/);
|
|
});
|
|
|
|
test("performance report triage retains measurement boundaries without demanding unsafe evidence", () => {
|
|
const guide = fs.readFileSync(path.resolve(__dirname, "../../docs/ISSUE_TRIAGE.md"), "utf8");
|
|
assert.match(guide, /Pulse process, container or whole host/);
|
|
assert.match(guide, /process-start CPU average is not a recent window/);
|
|
assert.match(guide, /database\nsize is not a write rate/);
|
|
assert.match(guide, /without making new collection a condition of reporting/);
|
|
assert.match(guide, /Do not request raw\nprofiles, heap dumps, databases or full process command lines/);
|
|
});
|
|
|
|
test("older-version reports cannot trigger event or scheduled retest posting", async () => {
|
|
const issue = {
|
|
number: 1200,
|
|
title: "Upgrade regression",
|
|
body: "## Feedback type\nRegression\n\n## Pulse version\n6.3.1\n",
|
|
labels: [{ name: "bug" }],
|
|
author_association: "NONE",
|
|
created_at: "2026-09-08T08:55:00Z",
|
|
};
|
|
const { github, calls } = createGithub({ latestVersion: "6.4.1", issues: [issue] });
|
|
const args = { github, context: createContext({ issue }), core: createCore() };
|
|
await triage.postRetestComment(args);
|
|
assert.deepEqual(await triage.postEligibleRetestComments({
|
|
...args, nowMs: Date.parse("2026-09-08T09:01:00Z"),
|
|
}), { eligibleCount: 0, postedCount: 0 });
|
|
// Retirement must not merely shift the public write to a different API.
|
|
for (const values of Object.values(calls)) assert.equal(values.length, 0);
|
|
});
|
|
|
|
test("label sync preserves community-owned retest state regardless of version", async () => {
|
|
for (const version of ["6.3.1", "6.4.1", "_No response_"]) {
|
|
const { github, calls } = createGithub({ latestVersion: "6.4.1" });
|
|
await syncLabels({
|
|
github, core: createCore(),
|
|
context: createContext({ issue: {
|
|
number: 1200, title: "Bug", body: `## Pulse version\n${version}\n`,
|
|
labels: [{ name: "bug" }, { name: "needs-retest-on-latest" }],
|
|
} }),
|
|
});
|
|
assert.ok(!calls.addLabels[0].labels.includes("needs-retest-on-latest"));
|
|
assert.ok(!calls.removeLabel.some(call => call.name === "needs-retest-on-latest"));
|
|
assert.equal(calls.createComment.length, 0);
|
|
}
|
|
});
|
|
|
|
test("postRetestComment skips reopened issues", async () => {
|
|
const { github, calls } = createGithub({ latestVersion: "6.0.1" });
|
|
const issue = {
|
|
number: 1471,
|
|
title: "Disk temperature at 0°C",
|
|
body: "## Feedback type\nBug / regression\n\n## Pulse version\n5.1.31\n",
|
|
labels: [],
|
|
author_association: "NONE",
|
|
};
|
|
|
|
await triage.postRetestComment({
|
|
github,
|
|
context: createContext({ action: "reopened", issue }),
|
|
core: createCore(),
|
|
});
|
|
|
|
assert.equal(calls.createComment.length, 0);
|
|
});
|
|
|
|
test("postRetestComment skips maintainer-authored issues", async () => {
|
|
const { github, calls } = createGithub({ latestVersion: "6.0.1" });
|
|
const issue = {
|
|
number: 1300,
|
|
title: "Maintainer split issue on 5.1.9",
|
|
body: "## Feedback type\nBug / regression\n\n## Pulse version\n5.1.9\n",
|
|
labels: [],
|
|
author_association: "OWNER",
|
|
};
|
|
|
|
await triage.postRetestComment({
|
|
github,
|
|
context: createContext({ action: "opened", issue }),
|
|
core: createCore(),
|
|
});
|
|
|
|
assert.equal(calls.createComment.length, 0);
|
|
});
|
|
|
|
test("normalizeVersion accepts a capitalised V prefix", () => {
|
|
const { normalizeVersion } = triage.internals;
|
|
|
|
// Regression: issue #1538 reported "V6.0.4" under the "Pulse version"
|
|
// heading and was wrongly labelled needs-version-info. The heading regexes
|
|
// are case-insensitive and captured "V6.0.4" correctly, but handed it to a
|
|
// case-sensitive normalizeVersion, so every extraction path returned null.
|
|
assert.equal(normalizeVersion("V6.0.4"), "6.0.4");
|
|
assert.equal(normalizeVersion("v6.0.4"), "6.0.4");
|
|
assert.equal(normalizeVersion("6.0.4"), "6.0.4");
|
|
assert.equal(normalizeVersion("V6.1.0-rc.4"), "6.1.0-rc.4");
|
|
|
|
assert.equal(normalizeVersion("V6"), null);
|
|
assert.equal(normalizeVersion("### Pulse version"), null);
|
|
});
|
|
|
|
test("extractPulseVersion reads a capitalised version under its heading", () => {
|
|
const { extractPulseVersion } = triage.internals;
|
|
|
|
assert.equal(
|
|
extractPulseVersion("[Bug]: something broke", "### Pulse version\nV6.0.4\n"),
|
|
"6.0.4"
|
|
);
|
|
});
|
|
|
|
test("structured Pulse version never falls back to an upgrade source or agent version", () => {
|
|
const { extractPulseVersion } = triage.internals;
|
|
for (const version of ["6.4", "6", "_No response_", ""]) {
|
|
assert.equal(extractPulseVersion(
|
|
"[Bug]: upgrade from rcourtman/pulse:v5.1.35 to rcourtman/pulse:6 failed",
|
|
`### Pulse version\n\n${version}\n\n### Agent version\n5.1.30\n`,
|
|
), null);
|
|
}
|
|
assert.equal(extractPulseVersion("upgrade from v5.1.35", "### Pulse version\r\n\r\nV6.4.1\r\n\r\n### Agent version\r\n5.1.30"), "6.4.1");
|
|
assert.equal(extractPulseVersion("bug on v6.4.1", "legacy free-form report"), "6.4.1");
|
|
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 = {
|
|
number: 1913,
|
|
title: "[Bug]: upgrade from rcourtman/pulse:v5.1.35 to rcourtman/pulse:6 failed",
|
|
body: "### Pulse version\n\n6.4\n\n### Agent version\nnone\n",
|
|
labels: [{ name: "bug" }],
|
|
author_association: "NONE",
|
|
};
|
|
const args = { github, context: createContext({ issue }), core: createCore() };
|
|
await syncLabels(args);
|
|
await triage.postRetestComment(args);
|
|
const labels = calls.addLabels.at(-1).labels;
|
|
assert.ok(labels.includes("needs-version-info"));
|
|
assert.ok(!labels.includes("affects-5.1.35"));
|
|
assert.ok(!labels.includes("needs-retest-on-latest"));
|
|
assert.equal(calls.createComment.length, 0);
|
|
});
|
|
|
|
test("queued label sync cannot overwrite newer Community label decisions", async () => {
|
|
for (const communityAdded of [true, false]) {
|
|
const { github } = createGithub();
|
|
const live = new Set(["bug", "affects-6.0.0", "needs-version-info"]);
|
|
if (communityAdded) live.add("needs-retest-on-latest");
|
|
live.add("operator-reviewed");
|
|
github.rest.issues.setLabels = async ({ labels }) => {
|
|
live.clear();
|
|
labels.forEach(label => live.add(label));
|
|
};
|
|
github.rest.issues.addLabels = async ({ labels }) => labels.forEach(label => live.add(label));
|
|
github.rest.issues.removeLabel = async ({ name }) => live.delete(name);
|
|
await syncLabels({ github, core: createCore(), context: createContext({ issue: {
|
|
number: 1200, title: "Bug", body: "## Pulse version\n6.0.1\n",
|
|
labels: ["bug", "affects-6.0.0", "needs-version-info",
|
|
...(!communityAdded ? ["needs-retest-on-latest"] : [])].map(name => ({ name })),
|
|
} }) });
|
|
assert.equal(live.has("needs-retest-on-latest"), communityAdded);
|
|
assert.ok(live.has("operator-reviewed"));
|
|
assert.ok(live.has("affects-6.0.1"));
|
|
assert.ok(!live.has("affects-6.0.0"));
|
|
assert.ok(!live.has("needs-version-info"));
|
|
}
|
|
});
|
|
|
|
test("version-label removal tolerates an absent label but propagates access failures", async () => {
|
|
for (const status of [404, 403]) {
|
|
const { github } = createGithub();
|
|
github.rest.issues.removeLabel = async () => { throw Object.assign(new Error("API failure"), { status }); };
|
|
const run = syncLabels({ github, core: createCore(), context: createContext({ issue: {
|
|
number: 1200, title: "Bug", body: "## Pulse version\n6.0.1\n",
|
|
labels: [{ name: "bug" }, { name: "affects-6.0.0" }],
|
|
} }) });
|
|
if (status === 404) await assert.doesNotReject(run);
|
|
else await assert.rejects(run, { status: 403 });
|
|
}
|
|
});
|
|
|
|
// Different software versions can coexist in the same upgrade report. Only
|
|
// the affected server field may determine the affects-* label.
|
|
test("stable-to-preview intake keeps baseline, agent and platform versions out of labels", async () => {
|
|
for (const [running, expected] of [
|
|
["6.5.0-rc.1", "affects-6.5.0-rc.1"],
|
|
["unknown", "needs-version-info"],
|
|
]) {
|
|
const { github, calls } = createGithub({ latestVersion: "6.5.0" });
|
|
const issue = {
|
|
number: 2400,
|
|
title: "[v6 pre-release]: upgraded from 6.4.5",
|
|
body: [
|
|
"### Feedback type", "Bug / regression",
|
|
"### Pulse version", running,
|
|
"### Last known working Pulse version", "6.4.1",
|
|
"### Agent version", "6.4.5",
|
|
"### Install path", "Upgrade from a stable v6 release",
|
|
"### OS / environment", "Debian 12 / SCALE 26.0.0-BETA.3",
|
|
"### Additional actionable topics", "None",
|
|
].join("\n\n"),
|
|
labels: [],
|
|
};
|
|
await syncLabels({ github, context: createContext({ issue }), core: createCore() });
|
|
assert.deepEqual(calls.addLabels.flatMap((call) => call.labels).sort(), [expected, "bug"].sort());
|
|
assert.equal(calls.createComment.length, 0);
|
|
}
|
|
});
|
|
|
|
test("interrupted-upgrade targets and recovered versions cannot supply the failing version", async () => {
|
|
for (const [failing, expected] of [
|
|
["6.4.5", "affects-6.4.5"],
|
|
["unknown", "needs-version-info"],
|
|
]) {
|
|
for (const formTitle of ["[Bug]:", "[v6 pre-release]:"]) {
|
|
const { github, calls } = createGithub({ latestVersion: "6.5.0" });
|
|
const bugForm = formTitle === "[Bug]:";
|
|
const issue = {
|
|
number: 2785,
|
|
title: `${formTitle} interrupted upgrade to 6.5.0`,
|
|
body: [
|
|
...(!bugForm ? ["### Feedback type", "Bug / regression"] : []),
|
|
"### Pulse version", failing,
|
|
"### Last known working Pulse version", "6.4.1",
|
|
"### Agent version", "6.5.0",
|
|
"### Upgrade attempt and recovery",
|
|
"Attempt: 6.4.5 → 6.5.0; automatic timer; 6 October 04:20 UTC",
|
|
"Last recorded result: signature verified, then Broken pipe; exit unknown",
|
|
"Observed stop: whole LXC; stop time/cause unknown",
|
|
"Recovery already performed: booted 7 October; current version 6.5.0",
|
|
"### Additional actionable topics", "None",
|
|
].join("\n\n"),
|
|
// GitHub applies the bug form's label; the preview form declares its
|
|
// classification in Feedback type instead. Reproduce both inputs.
|
|
labels: bugForm ? [{ name: "bug" }] : [],
|
|
};
|
|
await syncLabels({ github, context: createContext({ issue }), core: createCore() });
|
|
assert.deepEqual(calls.addLabels.flatMap((call) => call.labels).sort(),
|
|
(bugForm ? [expected] : [expected, "bug"]).sort());
|
|
assert.equal(calls.createComment.length, 0);
|
|
assert.equal(calls.getLatestRelease.length, 0);
|
|
}
|
|
}
|
|
});
|
|
|
|
test("delayed events classify the current report and labels, not an old version", async () => {
|
|
const eventIssue = {
|
|
number: 2800,
|
|
title: "Bug after upgrade from 6.4.5",
|
|
body: "### Pulse version\nunknown\n",
|
|
labels: [{ name: "bug" }],
|
|
updated_at: "2026-10-08T10:00:00Z",
|
|
};
|
|
const currentIssue = {
|
|
...eventIssue,
|
|
state: "open",
|
|
body: "### Pulse version\n6.5.0\n",
|
|
labels: ["bug", "affects-6.4.5", "needs-version-info",
|
|
"needs-retest-on-latest", "operator-reviewed"].map(name => ({ name })),
|
|
updated_at: "2026-10-08T11:00:00Z",
|
|
};
|
|
const { github, calls } = createGithub({ currentIssue });
|
|
await syncLabels({ github, context: createContext({ issue: eventIssue }), core: createCore() });
|
|
|
|
assert.deepEqual(calls.getIssue, Array(2).fill({ owner: "rcourtman", repo: "Pulse", issue_number: 2800 }));
|
|
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("a delayed declaration cannot restore a cleared decomposition task", async () => {
|
|
const eventIssue = {
|
|
number: 2801,
|
|
title: "Two report topics",
|
|
body: "### Additional actionable topics\nA separate workflow problem\n",
|
|
labels: [],
|
|
updated_at: "2026-10-08T10:00:00Z",
|
|
};
|
|
for (const body of [eventIssue.body, "### Additional actionable topics\nNone\n"]) {
|
|
const { github, calls } = createGithub({ currentIssue: {
|
|
...eventIssue, state: "open", body, updated_at: "2026-10-08T11:00:00Z",
|
|
} });
|
|
await syncLabels({ github, context: createContext({ issue: eventIssue }), core: createCore() });
|
|
assert.equal(calls.addLabels.length, 0);
|
|
assert.equal(calls.removeLabel.length, 0);
|
|
assert.equal(calls.createComment.length, 0);
|
|
}
|
|
});
|
|
|
|
test("current feedback classification supersedes a queued bug declaration", async () => {
|
|
const eventIssue = {
|
|
number: 2802, title: "Feedback", labels: [],
|
|
body: "### Feedback type\nBug / regression\n### Pulse version\n6.4.5\n",
|
|
};
|
|
const { github, calls } = createGithub({ currentIssue: {
|
|
...eventIssue, state: "open",
|
|
body: "### Feedback type\nDocumentation issue\n### Pulse version\n6.5.0\n",
|
|
} });
|
|
await syncLabels({ github, context: createContext({ issue: eventIssue }), core: createCore() });
|
|
assert.deepEqual(calls.addLabels.map(call => call.labels), [["documentation"]]);
|
|
assert.equal(calls.removeLabel.length, 0);
|
|
assert.equal(calls.createComment.length, 0);
|
|
});
|
|
|
|
test("a failed current-issue read stops without stale classification or mutation", async () => {
|
|
for (const status of [403, 404, 500]) {
|
|
const { github, calls } = createGithub();
|
|
github.rest.issues.get = async () => {
|
|
throw Object.assign(new Error("Read failed"), { status });
|
|
};
|
|
await assert.rejects(syncLabels({ github, core: createCore(), context: createContext({ issue: {
|
|
number: 2803, title: "Old bug", body: "### Pulse version\n6.4.5\n",
|
|
labels: [{ name: "bug" }],
|
|
} }) }), { status });
|
|
assert.equal(calls.getLabel.length, 0);
|
|
assert.equal(calls.createLabel.length, 0);
|
|
assert.equal(calls.addLabels.length, 0);
|
|
assert.equal(calls.removeLabel.length, 0);
|
|
assert.equal(calls.getLatestRelease.length, 0);
|
|
assert.equal(calls.createComment.length, 0);
|
|
}
|
|
});
|
|
|
|
test("a closed report or pull request receives no issue metadata changes", async () => {
|
|
const eventIssue = {
|
|
number: 2804, title: "Bug", body: "### Pulse version\n6.5.0\n",
|
|
labels: [{ name: "bug" }],
|
|
};
|
|
for (const current of [{ state: "closed" }, { state: "open", pull_request: {} }]) {
|
|
const { github, calls } = createGithub({ currentIssue: { ...eventIssue, ...current } });
|
|
await syncLabels({ github, core: createCore(), context: createContext({ issue: eventIssue }) });
|
|
assert.equal(calls.getLabel.length, 0);
|
|
assert.equal(calls.addLabels.length, 0);
|
|
assert.equal(calls.removeLabel.length, 0);
|
|
assert.equal(calls.getLatestRelease.length, 0);
|
|
assert.equal(calls.createComment.length, 0);
|
|
}
|
|
});
|
|
|
|
test("a mismatched current issue identity fails closed", async () => {
|
|
const { github, calls } = createGithub({ currentIssue: {
|
|
number: 9999, state: "open", title: "Another report",
|
|
body: "### Pulse version\n6.5.0\n", labels: [{ name: "bug" }],
|
|
} });
|
|
await assert.rejects(syncLabels({ github, core: createCore(), context: createContext({ issue: {
|
|
number: 2805, title: "Bug", labels: [{ name: "bug" }],
|
|
} }) }), /Current issue identity did not match/);
|
|
assert.equal(calls.getLabel.length, 0);
|
|
assert.equal(calls.addLabels.length, 0);
|
|
assert.equal(calls.removeLabel.length, 0);
|
|
assert.equal(calls.createComment.length, 0);
|
|
});
|
|
|
|
|
|
test("quoted and multiline inline-code versions cannot replace a legacy declaration", () => {
|
|
const { extractPulseVersion } = triage.internals;
|
|
for (const newline of ["\n", "\r\n"]) {
|
|
for (const copied of [
|
|
"> Copied old log\nPulse version: 6.4.5",
|
|
"Pasted text: `\nPulse version: 6.4.5\n`",
|
|
"Pasted text: **`\nPulse version: 6.4.5\n`**",
|
|
"- Copied old log\n Pulse version: 6.4.5",
|
|
"<pre>\nPulse version: 6.4.5\n</pre>",
|
|
]) {
|
|
const body = (copied + "\n\nPulse version: **`6.5.0`**").replaceAll("\n", newline);
|
|
assert.equal(extractPulseVersion("Upgrade from v6.4.5", body), "6.5.0", body);
|
|
assert.equal(extractPulseVersion("Bug", copied.replaceAll("\n", newline)), null, copied);
|
|
}
|
|
}
|
|
});
|
|
|
|
test("raw HTML and nested headings cannot declare form metadata", () => {
|
|
const { extractPulseVersion, classifyV6FeedbackType, classifyAdditionalActionableTopics } = triage.internals;
|
|
const fields = "### Pulse version\n6.4.5\n### Feedback type\nBug / regression\n" +
|
|
"### Additional actionable topics\nCopied template text.";
|
|
for (const copied of [
|
|
"<div>\n" + fields + "\n</div>",
|
|
"<pre>\n" + fields + "\n</pre>",
|
|
"<script>\n" + fields + "\n</script>",
|
|
fields.split("\n").map(line => "> " + line).join("\n"),
|
|
"- Copied template\n\n" + fields.split("\n").map(line => " " + line).join("\n"),
|
|
]) {
|
|
assert.equal(extractPulseVersion("No declared version", copied), null, copied);
|
|
assert.equal(classifyV6FeedbackType(copied), null, copied);
|
|
assert.equal(classifyAdditionalActionableTopics(copied), null, copied);
|
|
const body = copied + "\n\n### Pulse version\nunknown\n### Feedback type\nDocumentation issue\n" +
|
|
"### Additional actionable topics\nNone";
|
|
assert.equal(extractPulseVersion("Upgrade from v6.4.5", body), null, body);
|
|
assert.equal(classifyV6FeedbackType(body), "documentation", body);
|
|
assert.equal(classifyAdditionalActionableTopics(body), false, body);
|
|
}
|
|
});
|
|
|
|
test("the actual label consumer keeps copied declarations from changing report or Community state", async () => {
|
|
const { github, calls } = createGithub();
|
|
const issue = { number: 2902, title: "Pasted old installer output",
|
|
body: "> Copied log\nPulse version: 6.4.5\n\n" +
|
|
"<div>\n### Feedback type\nBug / regression\n### Additional actionable topics\nExample\n</div>\n\n" +
|
|
"Pulse version: `6.5.0`",
|
|
labels: ["bug", "affects-6.4.5", "needs-version-info", "needs-retest-on-latest", "needs-human", "operator-reviewed"]
|
|
.map(name => ({ name })) };
|
|
await syncLabels({ github, core: createCore(), context: createContext({ issue }) });
|
|
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.deepEqual(calls.createComment, []);
|
|
assert.deepEqual(calls.getLatestRelease, []);
|
|
assert.deepEqual(calls.paginate, []);
|
|
assert.deepEqual(calls.getIssue.map(call => call.issue_number), [2902, 2902]);
|
|
});
|
|
|
|
|
|
test("triage workflow prepares and checks the exact parser before minting its write token", () => {
|
|
const workflow = fs.readFileSync(path.resolve(__dirname, "../workflows/issue-version-label-sync.yml"), "utf8");
|
|
const install = workflow.indexOf("run: npm ci --ignore-scripts --no-audit --no-fund --prefix .github/scripts");
|
|
const check = workflow.indexOf("run: npm run test:triage --prefix .github/scripts");
|
|
const credential = workflow.indexOf("- name: Mint triage bot token");
|
|
assert.ok(install >= 0 && check > install && credential > check);
|
|
assert.match(workflow, /node-version: '24'/);
|
|
assert.match(workflow, /actions\/setup-node@[0-9a-f]{40}/);
|
|
assert.match(workflow, /persist-credentials: false/);
|
|
for (const file of ["package.json", "package-lock.json", "issue-version-triage.test.cjs", "triage-comment-policy.test.cjs"]) {
|
|
assert.ok(workflow.includes(`.github/scripts/${file}`), file);
|
|
}
|
|
assert.ok(workflow.includes("docs/ISSUE_TRIAGE.md"));
|
|
assert.ok(workflow.includes(".github/ISSUE_TEMPLATE"));
|
|
assert.ok(workflow.includes(".github/workflows"));
|
|
assert.doesNotMatch(workflow, /npm install|--ignore-scripts=false|\$\{\{ github\.event\.issue\.body/);
|
|
});
|
|
|
|
test("the standalone triage graph reuses the existing exact parser without transitive or lifecycle additions", () => {
|
|
const manifest = JSON.parse(fs.readFileSync(path.join(__dirname, "package.json"), "utf8"));
|
|
const lock = JSON.parse(fs.readFileSync(path.join(__dirname, "package-lock.json"), "utf8"));
|
|
assert.equal(manifest.private, true);
|
|
assert.deepEqual(manifest.dependencies, { marked: "17.0.6" });
|
|
assert.deepEqual(lock.packages[""].dependencies, manifest.dependencies);
|
|
assert.deepEqual(Object.keys(lock.packages).sort(), ["", "node_modules/marked"]);
|
|
const parser = lock.packages["node_modules/marked"];
|
|
assert.equal(parser.version, manifest.dependencies.marked);
|
|
assert.equal(parser.resolved, "https://registry.npmjs.org/marked/-/marked-17.0.6.tgz");
|
|
assert.match(parser.integrity, /^sha512-/);
|
|
assert.equal(parser.hasInstallScript, undefined);
|
|
assert.equal(parser.dependencies, undefined);
|
|
assert.equal(parser.optionalDependencies, undefined);
|
|
});
|
|
|
|
|
|
// Exercise the real mutation consumer, not just its Markdown classifier. A
|
|
// successful first read cannot authorize a stale write after label preparation.
|
|
for (const [scenario, changes] of [
|
|
["closure", { state: "closed" }],
|
|
["body correction", { body: "### Pulse version\n6.6.0\n" }],
|
|
["title correction", { title: "Corrected report" }],
|
|
["review activity", { updated_at: "2026-10-14T10:01:00Z" }],
|
|
["classification removal", { labels: [] }],
|
|
["human ownership", { labels: [{ name: "bug" }, { name: "needs-human" }] }],
|
|
["pull request", { pull_request: {} }],
|
|
]) {
|
|
test(`label preparation cannot outlive a report's ${scenario}`, async () => {
|
|
const issue = {
|
|
number: 2910, state: "open", title: "Current bug",
|
|
body: "### Pulse version\n6.5.0\n### Additional actionable topics\nA separate History problem\n",
|
|
updated_at: "2026-10-14T10:00:00Z",
|
|
labels: ["bug", "affects-6.4.5", "needs-version-info"].map(name => ({ name })),
|
|
};
|
|
const { github, calls } = createGithub({ currentIssue: structuredClone(issue) });
|
|
const read = github.rest.issues.get;
|
|
github.rest.issues.get = async payload => ({ data: structuredClone((await read(payload)).data) });
|
|
github.rest.issues.getLabel = async ({ name }) => {
|
|
calls.getLabel.push(name);
|
|
github.currentIssue = { ...github.currentIssue, ...structuredClone(changes) };
|
|
return { data: { name } };
|
|
};
|
|
const messages = [];
|
|
await syncLabels({ github, context: createContext({ issue }), core: { info: text => messages.push(text) } });
|
|
assert.equal(calls.getIssue.length, 2);
|
|
assert.equal(calls.addLabels.length, 0);
|
|
assert.equal(calls.removeLabel.length, 0);
|
|
assert.equal(calls.createComment.length, 0);
|
|
assert.equal(calls.getLatestRelease.length, 0);
|
|
assert.equal(calls.paginate.length, 0);
|
|
assert.ok(messages.includes("Issue changed during metadata preparation. Skipping outdated label delta."));
|
|
});
|
|
}
|
|
|
|
test("a non-bug classification also requires a current report before its write", async () => {
|
|
const issue = { number: 2911, state: "open", title: "Feedback", labels: [],
|
|
body: "### Feedback type\nDocumentation issue\n" };
|
|
const { github, calls } = createGithub({ currentIssue: issue });
|
|
const read = github.rest.issues.get;
|
|
github.rest.issues.get = async payload => {
|
|
const result = await read(payload);
|
|
return { data: calls.getIssue.length === 1 ? result.data : { ...issue, state: "closed" } };
|
|
};
|
|
await syncLabels({ github, context: createContext({ issue }), core: createCore() });
|
|
assert.equal(calls.getIssue.length, 2);
|
|
assert.equal(calls.addLabels.length, 0);
|
|
assert.equal(calls.removeLabel.length, 0);
|
|
});
|
|
|
|
for (const status of [403, 404, 500]) {
|
|
test(`a failed pre-write read (${status}) cannot use the initial snapshot`, async () => {
|
|
const issue = { number: 2912, state: "open", title: "Bug", labels: [{ name: "bug" }],
|
|
body: "### Pulse version\n6.5.0\n" };
|
|
const { github, calls } = createGithub({ currentIssue: issue });
|
|
const read = github.rest.issues.get;
|
|
github.rest.issues.get = async payload => {
|
|
const result = await read(payload);
|
|
if (calls.getIssue.length === 2) throw Object.assign(new Error("Read failed"), { status });
|
|
return result;
|
|
};
|
|
await assert.rejects(syncLabels({ github, context: createContext({ issue }), core: createCore() }), { status });
|
|
assert.equal(calls.getIssue.length, 2);
|
|
assert.equal(calls.addLabels.length, 0);
|
|
assert.equal(calls.removeLabel.length, 0);
|
|
assert.equal(calls.createComment.length, 0);
|
|
});
|
|
}
|
|
|
|
test("a mismatched pre-write issue identity fails closed", async () => {
|
|
const issue = { number: 2913, state: "open", title: "Bug", labels: [{ name: "bug" }],
|
|
body: "### Pulse version\n6.5.0\n" };
|
|
const { github, calls } = createGithub({ currentIssue: issue });
|
|
const read = github.rest.issues.get;
|
|
github.rest.issues.get = async payload => {
|
|
const result = await read(payload);
|
|
return { data: calls.getIssue.length === 1 ? result.data : { ...issue, number: 9999 } };
|
|
};
|
|
await assert.rejects(syncLabels({ github, context: createContext({ issue }), core: createCore() }),
|
|
/Current issue identity did not match/);
|
|
assert.equal(calls.addLabels.length, 0);
|
|
assert.equal(calls.removeLabel.length, 0);
|
|
});
|
|
|
|
test("the pre-write comparison treats label order as irrelevant", async () => {
|
|
const issue = { number: 2914, state: "open", title: "Bug",
|
|
labels: ["bug", "needs-human", "needs-retest-on-latest", "affects-6.4.5"].map(name => ({ name })),
|
|
body: "### Pulse version\n6.5.0\n" };
|
|
const { github, calls } = createGithub({ currentIssue: issue });
|
|
const read = github.rest.issues.get;
|
|
github.rest.issues.get = async payload => {
|
|
const result = await read(payload);
|
|
return { data: calls.getIssue.length === 1 ? result.data : { ...issue, labels: [...issue.labels].reverse() } };
|
|
};
|
|
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"]);
|
|
});
|
|
|
|
test("a report with no label delta needs no additional pre-write read", async () => {
|
|
const issue = { number: 2915, state: "open", title: "Bug",
|
|
labels: ["bug", "affects-6.5.0", "needs-human"].map(name => ({ name })),
|
|
body: "### Pulse version\n6.5.0\n" };
|
|
const { github, calls } = createGithub({ currentIssue: issue });
|
|
await syncLabels({ github, context: createContext({ issue }), core: createCore() });
|
|
assert.equal(calls.getIssue.length, 1);
|
|
assert.equal(calls.addLabels.length, 0);
|
|
assert.equal(calls.removeLabel.length, 0);
|
|
});
|
|
|
|
test("unknown current versions retire only managed exact-version labels", async () => {
|
|
const preserved = ["bug", "needs-retest-on-latest", "needs-decomposition",
|
|
"needs-human", "operator-reviewed", "affects-main", "affects-storage"];
|
|
for (const body of [
|
|
"### Pulse version\nunknown\n",
|
|
"Pulse version: unknown; agent version 6.5.0",
|
|
"### Pulse version\n6.5\n### Agent version\n6.5.0",
|
|
"### Pulse version\n_No response_\n",
|
|
"### Pulse version\n\n### Agent version\n6.5.0",
|
|
"The running version is unavailable.",
|
|
]) {
|
|
const { github, calls } = createGithub();
|
|
const issue = { number: 2916, title: "Upgrade from v6.4.5", body,
|
|
labels: [...preserved, "affects-6.4.5", "affects-6.6.0-beta.3"].map(name => ({ name })) };
|
|
await syncLabels({ github, context: createContext({ action: "edited", issue }), core: createCore() });
|
|
assert.deepEqual(calls.addLabels.map(call => call.labels), [["needs-version-info"]], body);
|
|
assert.deepEqual(calls.removeLabel.map(call => call.name), ["affects-6.4.5", "affects-6.6.0-beta.3"], body);
|
|
assert.equal(calls.getIssue.length, 2);
|
|
assert.equal(calls.getLatestRelease.length, 0);
|
|
assert.equal(calls.createComment.length, 0);
|
|
}
|
|
});
|
|
|
|
test("a delayed known-version event cleans metadata from the current unknown report", async () => {
|
|
const eventIssue = { number: 2917, title: "Bug", body: "### Pulse version\n6.4.5\n",
|
|
labels: [{ name: "bug" }, { name: "affects-6.4.5" }] };
|
|
const { github, calls } = createGithub({ currentIssue: {
|
|
...eventIssue, state: "open", body: "### Pulse version\nunknown\n",
|
|
updated_at: "2026-10-14T11:00:00Z",
|
|
labels: [...eventIssue.labels, { name: "needs-retest-on-latest" }, { name: "needs-version-info" }],
|
|
} });
|
|
await syncLabels({ github, context: createContext({ action: "edited", issue: eventIssue }), core: createCore() });
|
|
assert.equal(calls.addLabels.length, 0);
|
|
assert.deepEqual(calls.removeLabel.map(call => call.name), ["affects-6.4.5"]);
|
|
assert.equal(calls.getIssue.length, 2, "a removals-only delta still needs the live-state fence");
|
|
assert.equal(calls.createComment.length, 0);
|
|
});
|
|
|
|
test("unknown-version removal cannot overwrite a correction during preparation", async () => {
|
|
const issue = { number: 2918, title: "Bug", body: "### Pulse version\nunknown\n",
|
|
labels: ["bug", "affects-6.5.0", "needs-version-info"].map(name => ({ name })) };
|
|
const { github, calls } = createGithub();
|
|
github.rest.issues.getLabel = async () => {
|
|
github.currentIssue = { ...github.currentIssue, body: "### Pulse version\n6.5.0\n" };
|
|
return { data: {} };
|
|
};
|
|
await syncLabels({ github, context: createContext({ action: "edited", issue }), core: createCore() });
|
|
assert.equal(calls.getIssue.length, 2);
|
|
assert.equal(calls.addLabels.length, 0);
|
|
assert.equal(calls.removeLabel.length, 0);
|
|
});
|
|
|
|
test("unknown versions do not remove metadata from non-bug feedback", async () => {
|
|
const { github, calls } = createGithub();
|
|
const issue = { number: 2919, title: "Documentation", body: "### Pulse version\nunknown\n",
|
|
labels: ["documentation", "affects-6.5.0", "needs-retest-on-latest"].map(name => ({ name })) };
|
|
await syncLabels({ github, context: createContext({ issue }), core: createCore() });
|
|
assert.equal(calls.addLabels.length, 0);
|
|
assert.equal(calls.removeLabel.length, 0);
|
|
assert.equal(calls.getIssue.length, 1);
|
|
});
|
|
|
|
test("already unknown-version metadata is a no-op without an extra live read", async () => {
|
|
const { github, calls } = createGithub({ existingLabels: new Set(["needs-version-info"]) });
|
|
const issue = { number: 2920, title: "Bug", body: "### Pulse version\nunknown\n",
|
|
labels: ["bug", "needs-version-info", "needs-human", "affects-main"].map(name => ({ name })) };
|
|
await syncLabels({ github, context: createContext({ issue }), core: createCore() });
|
|
assert.equal(calls.addLabels.length, 0);
|
|
assert.equal(calls.removeLabel.length, 0);
|
|
assert.equal(calls.getIssue.length, 1);
|
|
});
|
|
|
|
test("attempted server archives separate release versions from packaging", () => {
|
|
const { extractPulseVersion } = triage.internals;
|
|
for (const version of ["6.5.0", "6.5.0-rc.1", "6.6.0-beta.3", "6.6.0-dev.branch-2"]) {
|
|
for (const filename of [
|
|
...["386", "amd64", "arm64", "armv6", "armv7"].map(arch =>
|
|
`pulse-v${version}-linux-${arch}.tar.gz`),
|
|
`pulse-v${version}.tar.gz`,
|
|
`pulse-${version}.tgz`,
|
|
]) {
|
|
for (const body of [
|
|
`### Pulse version\n${filename}\n### Agent version\nnone`,
|
|
`Pulse version: **\`${filename}\`**`,
|
|
]) {
|
|
assert.equal(extractPulseVersion("Failed install", body), version, filename);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
test("non-server artifacts cannot supply an attempted server version", () => {
|
|
const { extractPulseVersion } = triage.internals;
|
|
for (const filename of [
|
|
"pulse-agent-v6.6.0-beta.3-linux-amd64.tar.gz",
|
|
"pulse-agent-helper-v6.6.0-beta.3-linux-amd64.tar.gz",
|
|
"pulse-agent-runner-v6.6.0-beta.3-linux-amd64.tar.gz",
|
|
"pulse-mcp-v6.6.0-beta.3-windows-amd64.zip",
|
|
"pulse-provider-msp-v6.6.0-beta.3.tar.gz",
|
|
"pulse-v6.6.0-beta.3-linux-amd64.tar.gz.sha256",
|
|
"pulse-v6.6.0-beta.3-linux-amd64.tar.gz.sig",
|
|
"pulse-v6.6.0-beta.3-linux-amd64.tar.gz.sshsig",
|
|
"pulse-v6.6.0-beta.3-release.sbom.spdx.json.sha256",
|
|
"install.sh",
|
|
]) {
|
|
assert.equal(extractPulseVersion("Upgrade from v6.4.5",
|
|
`### Pulse version\n${filename}\n### Agent version\n6.5.0`), null, filename);
|
|
}
|
|
});
|
|
|
|
test("archive recognition neither borrows a later filename nor shortens bare prereleases", () => {
|
|
const { extractPulseVersion } = triage.internals;
|
|
assert.equal(extractPulseVersion("Upgrade from v6.4.5",
|
|
"### Pulse version\nunknown; downloaded pulse-v6.6.0-beta.3-linux-amd64.tar.gz"), null);
|
|
for (const version of ["6.6.0-dev.branch-2", "6.6.0-beta.3-linux-amd64", "6.6.0-custom.tgz"]) {
|
|
assert.equal(extractPulseVersion("Bug", `### Pulse version\n${version}`), version);
|
|
}
|
|
});
|
|
|
|
for (const [filename, version] of [
|
|
["pulse-v6.6.0-beta.3-linux-amd64.tar.gz", "6.6.0-beta.3"],
|
|
["pulse-v6.6.0-beta.3-linux-armv7.tar.gz", "6.6.0-beta.3"],
|
|
["pulse-v6.6.0-beta.3.tar.gz", "6.6.0-beta.3"],
|
|
["pulse-6.6.0-beta.3.tgz", "6.6.0-beta.3"],
|
|
["pulse-v6.5.0-linux-amd64.tar.gz", "6.5.0"],
|
|
]) {
|
|
test(`archive intake reconciles exact-version labels silently: ${filename}`, async () => {
|
|
const { github, calls } = createGithub();
|
|
const oldArtifactLabel = `affects-${filename.replace(/^pulse-v?/, "")}`;
|
|
const issue = { number: 2921, title: "[Bug]: Failed install",
|
|
body: `### Pulse version\n${filename}\n### Agent version\nnone`,
|
|
labels: ["bug", "affects-6.4.5", oldArtifactLabel, "needs-version-info", "needs-human",
|
|
"needs-retest-on-latest"].map(name => ({ name })) };
|
|
await syncLabels({ github, context: createContext({ issue }), core: createCore() });
|
|
assert.deepEqual(calls.addLabels.map(call => call.labels), [[`affects-${version}`]]);
|
|
assert.deepEqual(calls.removeLabel.map(call => call.name),
|
|
["affects-6.4.5", oldArtifactLabel, "needs-version-info"]);
|
|
assert.deepEqual(calls.createLabel.map(call => call.name), [`affects-${version}`]);
|
|
assert.deepEqual(calls.createComment, []);
|
|
assert.deepEqual(calls.getLatestRelease, []);
|
|
assert.deepEqual(calls.paginate, []);
|
|
assert.equal(calls.getIssue.length, 2);
|
|
});
|
|
}
|