Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 138 additions & 21 deletions .github/scripts/release-qa-status/src/format.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { renderSlack } from './format';
import { ReleaseQAReport, PRQAResult } from './types';
import { renderMarkdown, renderSlack } from './format';
import { PRQAResult, ReleaseQAReport, SlackMapping } from './types';

function pr(
number: number,
Expand Down Expand Up @@ -59,7 +59,7 @@ describe('renderSlack', () => {
expect(renderSlack(r)).toContain(':rotating_light: *QA Coverage*');
});

it('uses the requested count labels', () => {
it('renders the count labels exactly as requested', () => {
const r = report({
summary: { failed: 1, missing: 2, unlinked: 3, external: 4, passed: 0, excluded: 0 },
failed: [pr(1, 't', 'failed')],
Expand All @@ -71,35 +71,152 @@ describe('renderSlack', () => {
expect(out).toContain('Not in the core repo: 4');
});

it('truncates very long titles with an ellipsis', () => {
const longTitle = 'x'.repeat(200);
it('does NOT include a detailed PR list in the slack snippet', () => {
const r = report({
summary: { failed: 0, missing: 1, unlinked: 0, external: 0, passed: 0, excluded: 0 },
missing: [pr(10, longTitle, 'missing')],
summary: { failed: 0, missing: 2, unlinked: 0, external: 0, passed: 0, excluded: 0 },
missing: [
pr(35463, 'feat(maintenance): thread dump endpoints', 'missing'),
pr(35726, 'fix(edit-content): normalize lockedBy', 'missing'),
],
});
const out = renderSlack(r);
expect(out).toContain('…');
// Original 200-char title should not appear verbatim
expect(out).not.toContain(longTitle);
// Compact: just counts + cc line, no per-PR lines.
expect(out).not.toContain('thread dump');
expect(out).not.toContain('normalize lockedBy');
expect(out).not.toContain('https://github.com/dotCMS/core/pull/35463');
});

it('escapes mrkdwn-meaningful characters in titles', () => {
it('wraps each count in a Slack link with per-bucket anchor when detailUrl is provided', () => {
const r = report({
summary: { failed: 0, missing: 1, unlinked: 0, external: 0, passed: 0, excluded: 0 },
missing: [pr(10, 'fix <script> & co.', 'missing')],
summary: { failed: 1, missing: 0, unlinked: 0, external: 0, passed: 0, excluded: 0 },
failed: [pr(1, 't', 'failed')],
});
const out = renderSlack(r);
expect(out).toContain('&lt;script&gt;');
expect(out).toContain('&amp;');
const out = renderSlack(r, { detailUrl: 'https://example.com/run/123' });
expect(out).toContain('<https://example.com/run/123#qa-failed|failed QA: 1>');
expect(out).toContain('<https://example.com/run/123#qa-missing|missing QA: 0>');
expect(out).toContain('<https://example.com/run/123#qa-orphan|Orphan PRs: 0>');
expect(out).toContain('<https://example.com/run/123#qa-external|Not in the core repo: 0>');
});

it('replaces backticks so paired backticks do not render as Slack monospace', () => {
it('omits detailUrl wrapping when none is provided', () => {
const r = report({
summary: { failed: 0, missing: 1, unlinked: 0, external: 0, passed: 0, excluded: 0 },
missing: [pr(10, 'bump deps to `v1.2.3-rc.1`', 'missing')],
summary: { failed: 1, missing: 0, unlinked: 0, external: 0, passed: 0, excluded: 0 },
failed: [pr(1, 't', 'failed')],
});
const out = renderSlack(r);
expect(out).not.toContain('`v1.2.3-rc.1`');
expect(out).toContain("'v1.2.3-rc.1'");
expect(out).toContain('failed QA: 1');
expect(out).not.toMatch(/<https:[^|]+\|failed QA/);
});
});

describe('renderSlack cc line', () => {
const mappings: SlackMapping[] = [
{ github_user: 'alice', slack_id: 'U0ALICE' },
{ github_user: 'dsilvam', slack_id: 'U0DSILVAM' },
];

it('emits a cc line that mentions flagged-PR authors', () => {
const r = report({
summary: { failed: 1, missing: 0, unlinked: 0, external: 0, passed: 0, excluded: 0 },
failed: [pr(1, 't', 'failed', { author: 'alice' })],
});
const out = renderSlack(r, { mappings });
expect(out).toContain('cc <@U0ALICE>');
expect(out).toContain('please review your PRs');
});

it('uses Slack ID for mapped users and plain @login for unmapped', () => {
const r = report({
summary: { failed: 1, missing: 1, unlinked: 0, external: 0, passed: 0, excluded: 0 },
failed: [pr(1, 't', 'failed', { author: 'alice' })],
missing: [pr(2, 't', 'missing', { author: 'unknownuser' })],
});
const out = renderSlack(r, { mappings });
expect(out).toContain('<@U0ALICE>');
expect(out).toContain('@unknownuser');
expect(out).not.toContain('<@U0UNKNOWNUSER>');
});

it('deduplicates authors that have multiple flagged PRs', () => {
const r = report({
summary: { failed: 0, missing: 2, unlinked: 0, external: 0, passed: 0, excluded: 0 },
missing: [
pr(1, 't1', 'missing', { author: 'alice' }),
pr(2, 't2', 'missing', { author: 'alice' }),
],
});
const out = renderSlack(r, { mappings });
const occurrences = (out.match(/U0ALICE/g) || []).length;
expect(occurrences).toBe(1);
});

it('does NOT mention authors whose only PRs are in the passed bucket', () => {
const r = report({
summary: { failed: 1, missing: 0, unlinked: 0, external: 0, passed: 1, excluded: 0 },
failed: [pr(1, 't', 'failed', { author: 'alice' })],
passed: [pr(2, 't', 'passed', { author: 'dsilvam' })],
});
const out = renderSlack(r, { mappings });
expect(out).toContain('<@U0ALICE>');
expect(out).not.toContain('<@U0DSILVAM>');
});

it('matches mappings case-insensitively', () => {
const r = report({
summary: { failed: 1, missing: 0, unlinked: 0, external: 0, passed: 0, excluded: 0 },
failed: [pr(1, 't', 'failed', { author: 'Alice' })],
});
const out = renderSlack(r, { mappings });
expect(out).toContain('<@U0ALICE>');
});
});

describe('renderMarkdown', () => {
it('produces a markdown report with the expected H1 and summary table', () => {
const r = report({
summary: { failed: 0, missing: 1, unlinked: 0, external: 0, passed: 0, excluded: 0 },
missing: [pr(10, 'fix something', 'missing', { author: 'alice' })],
});
const out = renderMarkdown(r);
expect(out).toContain('# Release QA report: `v26.05.18-01` → `v26.05.19-01`');
expect(out).toContain('| :warning: Missing QA |');
expect(out).toContain('## :warning: No QA label (1)');
expect(out).toContain('[#10](https://github.com/dotCMS/core/pull/10)');
});

it('emits stable HTML anchors before each populated bucket heading', () => {
const r = report({
summary: { failed: 1, missing: 1, unlinked: 1, external: 1, passed: 0, excluded: 0 },
failed: [pr(1, 't', 'failed')],
missing: [pr(2, 't', 'missing')],
unlinked: [pr(3, 't', 'unlinked')],
external: [
pr(4, 't', 'external', {
externalRefs: [{ repo: 'dotCMS/private-issues', number: 9 }],
}),
],
});
const out = renderMarkdown(r);
expect(out).toContain('<a id="qa-failed"></a>');
expect(out).toContain('<a id="qa-missing"></a>');
expect(out).toContain('<a id="qa-orphan"></a>');
expect(out).toContain('<a id="qa-external"></a>');
});

it('skips anchors for empty buckets', () => {
const r = report({
summary: { failed: 1, missing: 0, unlinked: 0, external: 0, passed: 0, excluded: 0 },
failed: [pr(1, 't', 'failed')],
});
const out = renderMarkdown(r);
expect(out).toContain('<a id="qa-failed"></a>');
expect(out).not.toContain('<a id="qa-missing"></a>');
expect(out).not.toContain('<a id="qa-orphan"></a>');
expect(out).not.toContain('<a id="qa-external"></a>');
});

it('shows the all-clean message when nothing is flagged', () => {
const out = renderMarkdown(report());
expect(out).toContain('All non-excluded PRs have a recognized QA verdict');
});
});
Loading
Loading