Files
PowerToys/.github/scripts/pr-intake/tests/pr-intake.test.mjs
Niels Laute 523409ed06 Add AI-assisted PR triage (#49911)
## Summary of the Pull Request

Adds AI-assisted pull request triage using GitHub Agentic Workflows. A
bounded Copilot pass summarizes each PR and classifies screenshots,
GIFs, or video as required, recommended, or unnecessary. Deterministic
publishing validates the exact PR evidence hash, closing issue
references, merge conflicts, draft state, and supplied visual evidence
before updating one canonical comment.

The workflow manages only `Needs-Review` and `Needs-Author-Feedback`.
Missing issue references are advisory; invalid references, merge
conflicts, and missing required visual evidence block readiness.
Existing resource-management automation continues to close inactive PRs
awaiting author feedback.

## PR Checklist

- [ ] Closes: #xxx
- [x] **Communication:** Discussed the intended PR intake flow and
comment format
- [x] **Tests:** Added/updated and all pass
- [x] **Localization:** No product strings added
- [x] **Dev docs:** Added workflow documentation
- [x] **New binaries:** Not applicable
- [x] **Documentation updated:** Repository automation documentation
updated

## Validation Steps Performed

- `node --check .github/scripts/pr-intake/pr-intake.mjs`
- `node --test .github/scripts/pr-intake/tests/pr-intake.test.mjs` — 23
passing
- `gh aw compile pr-intake` with gh-aw v0.86.2
- Read-only preprocessing against PR #49905 confirmed four changed
files, no visual-evidence hint, and `mergeable=false` /
`mergeable_state=dirty` is detected as a blocking conflict

Copilot-Session: 3067a641-aa79-4f96-8d9f-eaa1c6d9b3cf
2026-08-14 21:29:14 +00:00

663 lines
22 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import {
ACTIONS_BOT_ID,
ACTIONS_BOT_LOGIN,
CANONICAL_MARKER,
NEEDS_AUTHOR_FEEDBACK_LABEL,
READY_FOR_REVIEW_LABEL,
ApiError,
buildIntakeReport,
classifyChangedPaths,
deriveVisualAssessment,
determineFeedbackSince,
findClosingIssueReferences,
findVisualEvidence,
getPullRequestWithMergeability,
hasMergeConflict,
isMergeabilityKnown,
parseFeedbackSince,
planManagedLabelChanges,
renderAllClearComment,
renderIntakeComment,
runPullRequestIntake,
selectCanonicalComment,
upsertCanonicalComment,
verifyClosingIssueReferences,
} from '../pr-intake.mjs';
function botComment(id, body) {
return {
id,
body,
user: {
login: ACTIONS_BOT_LOGIN,
id: ACTIONS_BOT_ID,
type: 'Bot',
},
};
}
class MockApi {
constructor({
comments = [],
issues = [],
pullRequests = [],
files = [],
pullRequestQueue = null,
} = {}) {
this.comments = comments;
this.issues = issues;
this.pullRequests = pullRequests;
this.files = files;
this.pullRequestQueue = pullRequestQueue;
this.created = 0;
this.updated = 0;
this.deleted = [];
this.addedLabels = [];
this.removedLabels = [];
this.getIssueCalls = 0;
this.getPullRequestCalls = 0;
this.nextCommentId = 1000;
}
async listIssueComments(_issueNumber, page) {
return page === 1 ? this.comments : [];
}
async createIssueComment(_issueNumber, body) {
this.created += 1;
const comment = botComment(this.nextCommentId, body);
this.nextCommentId += 1;
this.comments.push(comment);
return comment;
}
async updateIssueComment(commentId, body) {
this.updated += 1;
const comment = this.comments.find((entry) => entry.id === commentId);
comment.body = body;
return comment;
}
async deleteIssueComment(commentId) {
this.deleted.push(commentId);
this.comments = this.comments.filter((entry) => entry.id !== commentId);
return null;
}
async getIssue(issueNumber) {
this.getIssueCalls += 1;
const issue = this.issues.find((entry) => entry.number === issueNumber);
if (!issue) {
throw new ApiError('Not Found', 404);
}
return issue;
}
async getPullRequest(issueNumber) {
this.getPullRequestCalls += 1;
if (Array.isArray(this.pullRequestQueue) && this.pullRequestQueue.length) {
return this.pullRequestQueue.shift();
}
return this.pullRequests.find((entry) => entry.number === issueNumber)
?? { number: issueNumber, draft: false };
}
async listPullRequestFiles(_pullNumber, page) {
return page === 1 ? this.files : [];
}
async addLabels(issueNumber, labels) {
this.addedLabels.push({ issueNumber, labels });
return null;
}
async removeLabel(issueNumber, label) {
this.removedLabels.push({ issueNumber, label });
return null;
}
}
test('changed paths provide a visual hint for product UI files', () => {
const report = classifyChangedPaths([
'src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml',
'doc/devdocs/core/architecture.md',
]);
assert.equal(report.requiresVisualEvidence, true);
assert.deepEqual(report.visualCandidatePaths, [
'src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml',
]);
});
test('docs-only changes do not require visual evidence', () => {
const report = classifyChangedPaths([
'README.md',
'doc/devdocs/core/architecture.md',
]);
assert.equal(report.requiresVisualEvidence, false);
assert.deepEqual(report.categories, ['docs']);
});
test('deriveVisualAssessment maps the path hint to a requirement', () => {
assert.deepEqual(deriveVisualAssessment(true).visualEvidenceRequirement, 'REQUIRED');
assert.deepEqual(deriveVisualAssessment(false).visualEvidenceRequirement, 'NOT_NEEDED');
assert.match(deriveVisualAssessment(true).visualEvidenceReason, /product UI/);
});
test('merge conflicts are detected from GitHub mergeability fields', () => {
assert.equal(hasMergeConflict({ mergeable: false }), true);
assert.equal(hasMergeConflict({ mergeable_state: 'dirty' }), true);
assert.equal(hasMergeConflict({ mergeStateStatus: 'CONFLICTING' }), true);
assert.equal(hasMergeConflict({ mergeable: true, mergeable_state: 'clean' }), false);
assert.equal(hasMergeConflict({ mergeable: null, mergeable_state: 'unknown' }), false);
});
test('mergeability is only known once GitHub reports a definite state', () => {
assert.equal(isMergeabilityKnown({ mergeable: true }), true);
assert.equal(isMergeabilityKnown({ mergeable: false }), true);
assert.equal(isMergeabilityKnown({ mergeable_state: 'clean' }), true);
assert.equal(isMergeabilityKnown({ mergeable: null, mergeable_state: 'unknown' }), false);
assert.equal(isMergeabilityKnown({ mergeable: null }), false);
});
test('getPullRequestWithMergeability retries while mergeability is unknown', async () => {
const api = new MockApi({
pullRequestQueue: [
{ number: 7, mergeable: null, mergeable_state: 'unknown' },
{ number: 7, mergeable: null, mergeable_state: 'unknown' },
{ number: 7, mergeable: true, mergeable_state: 'clean' },
],
});
const result = await getPullRequestWithMergeability(api, 7, { delayMs: 0 });
assert.equal(result.mergeabilityKnown, true);
assert.equal(result.pullRequest.mergeable, true);
assert.equal(api.getPullRequestCalls, 3);
});
test('getPullRequestWithMergeability gives up after the attempt cap', async () => {
const api = new MockApi({
pullRequestQueue: [
{ number: 7, mergeable: null, mergeable_state: 'unknown' },
{ number: 7, mergeable: null, mergeable_state: 'unknown' },
],
});
const result = await getPullRequestWithMergeability(api, 7, {
maxAttempts: 2,
delayMs: 0,
});
assert.equal(result.mergeabilityKnown, false);
assert.equal(api.getPullRequestCalls, 2);
});
test('closing issue parsing finds supported keywords and de-duplicates issue numbers', () => {
const references = findClosingIssueReferences(
'Fixes #12\nResolved: #12\nCloses owner/repo#44',
);
assert.deepEqual(references, [
{ keyword: 'fixes', issueNumber: 12, repositoryFullName: null },
{ keyword: 'closes', issueNumber: 44, repositoryFullName: 'owner/repo' },
]);
});
test('closing issue parsing caps the number of references it accepts', () => {
const body = Array.from({ length: 50 }, (_unused, index) => `Closes #${index + 1}`).join('\n');
const references = findClosingIssueReferences(body);
assert.equal(references.length, 20);
});
test('closing issue verification is bounded and preserves reference order', async () => {
const api = new MockApi({
issues: Array.from({ length: 20 }, (_unused, index) => ({
number: index + 1,
title: `Issue ${index + 1}`,
})),
});
const references = Array.from({ length: 20 }, (_unused, index) => ({
keyword: 'closes',
issueNumber: index + 1,
repositoryFullName: null,
}));
const result = await verifyClosingIssueReferences({
api,
repositoryFullName: 'microsoft/PowerToys',
references,
});
assert.equal(result.validReferences.length, 20);
assert.deepEqual(
result.validReferences.map((entry) => entry.issueNumber),
references.map((entry) => entry.issueNumber),
);
});
test('closing issue verification accepts local issues and rejects pull requests, other repos, and 404s', async () => {
const api = new MockApi({
issues: [
{ number: 12, title: 'Tracked bug' },
{ number: 55, title: 'Feature PR', pull_request: { url: 'https://example.test/pr/55' } },
],
});
const result = await verifyClosingIssueReferences({
api,
repositoryFullName: 'microsoft/PowerToys',
references: [
{ keyword: 'closes', issueNumber: 12, repositoryFullName: null },
{ keyword: 'fixes', issueNumber: 55, repositoryFullName: null },
{ keyword: 'resolves', issueNumber: 99, repositoryFullName: null },
{ keyword: 'closes', issueNumber: 44, repositoryFullName: 'other/repo' },
],
});
assert.deepEqual(result.validReferences, [
{ keyword: 'closes', issueNumber: 12, repositoryFullName: null, title: 'Tracked bug' },
]);
assert.deepEqual(result.invalidReferences, [
{ keyword: 'fixes', issueNumber: 55, repositoryFullName: null, reason: 'pull-request' },
{ keyword: 'resolves', issueNumber: 99, repositoryFullName: null, reason: 'not-found' },
{ keyword: 'closes', issueNumber: 44, repositoryFullName: 'other/repo', reason: 'different-repository' },
]);
});
test('visual evidence detection recognizes markdown, attachments, and video links', () => {
const evidence = findVisualEvidence(`
![Screenshot](https://example.com/screenshot.png)
https://github.com/user-attachments/assets/12345678-1234-1234-1234-123456789abc
https://www.youtube.com/watch?v=demo123
`);
assert.equal(evidence.found, true);
assert.deepEqual(evidence.types, [
'GitHub user-attachment URL',
'Markdown image',
'Recognized video link',
]);
});
test('non-visual GitHub file attachments do not satisfy visual evidence', () => {
const evidence = findVisualEvidence(`
https://github.com/user-attachments/files/1234/PowerToysReport_demo.zip
![fake](https://github.com/user-attachments/files/1234/PowerToysReport_demo.zip)
<img src="https://github.com/user-attachments/files/1234/PowerToysReport_demo.zip">
`);
assert.equal(evidence.found, false);
assert.deepEqual(evidence.types, []);
});
test('label plan removes only managed lifecycle labels', () => {
const plan = planManagedLabelChanges(
['Product-FancyZones', NEEDS_AUTHOR_FEEDBACK_LABEL],
[READY_FOR_REVIEW_LABEL],
[READY_FOR_REVIEW_LABEL, NEEDS_AUTHOR_FEEDBACK_LABEL],
);
assert.deepEqual(plan, {
add: [READY_FOR_REVIEW_LABEL],
remove: [NEEDS_AUTHOR_FEEDBACK_LABEL],
});
});
test('incomplete comment mentions the author and shows only actionable bullets', () => {
const report = buildIntakeReport({
changedPaths: ['src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml'],
body: '',
repositoryHtmlUrl: 'https://github.com/microsoft/PowerToys',
baseRef: 'main',
authorLogin: 'alice',
verifiedClosingIssues: [],
invalidClosingIssues: [
{ keyword: 'closes', issueNumber: 999, repositoryFullName: null, reason: 'not-found' },
],
});
const body = renderIntakeComment(report, '2026-08-05T10:00:00.000Z');
assert.match(body, /^<!-- powertoys-pr-intake:canonical:v1 -->/);
assert.match(body, /## 🧭 PR intake/);
assert.match(body, /Visual evidence:\*\* Required/);
assert.match(body, /@alice, please update/);
assert.match(body, /invalid closing reference `#999` \(not found\)/);
assert.match(body, /Closes #123/);
assert.match(body, /Replace the invalid closing reference/);
assert.match(body, /screenshot, GIF, or video/);
assert.match(body, /no author response within 7 days/);
assert.equal(parseFeedbackSince(body), '2026-08-05T10:00:00.000Z');
assert.match(body, /\[contribution guide\]\(https:\/\/github\.com\/microsoft\/PowerToys\/blob\/main\/CONTRIBUTING\.md\)/);
assert.doesNotMatch(body, /Summary:|Ownership matches|Managed labels|Routing|Files scanned/);
});
test('complete intake renders the ready state without a summary line', () => {
const report = buildIntakeReport({
changedPaths: ['doc/devdocs/core/architecture.md'],
body: 'Closes #12',
repositoryHtmlUrl: 'https://github.com/microsoft/PowerToys',
baseRef: 'main',
authorLogin: 'alice',
verifiedClosingIssues: [{ issueNumber: 12, title: 'Tracked issue' }],
invalidClosingIssues: [],
});
const body = renderIntakeComment(report);
assert.equal(report.readyForReview, true);
assert.match(body, /## ✅ Ready for review/);
assert.match(body, /Visual evidence:\*\* Not needed/);
assert.doesNotMatch(body, /Summary:|@alice|contribution guide|Products|Routing/);
});
test('missing issue link is recommended without blocking readiness', () => {
const report = buildIntakeReport({
changedPaths: ['doc/devdocs/core/architecture.md'],
body: 'Clarifies the contributor checklist.',
repositoryHtmlUrl: 'https://github.com/microsoft/PowerToys',
baseRef: 'main',
authorLogin: 'alice',
verifiedClosingIssues: [],
invalidClosingIssues: [],
});
assert.equal(report.readyForReview, true);
assert.equal(report.needsAuthorFeedback, false);
assert.deepEqual(report.authorActions, []);
assert.deepEqual(report.recommendations, [
'Link the issue this PR fixes using a closing keyword such as `Closes #123`.',
]);
assert.match(renderIntakeComment(report), /## Recommendation/);
});
test('merge conflict blocks readiness with an author action', () => {
const report = buildIntakeReport({
changedPaths: ['.github/workflows/issue-triage.md'],
body: 'Closes #12',
repositoryHtmlUrl: 'https://github.com/microsoft/PowerToys',
baseRef: 'main',
authorLogin: 'alice',
mergeConflict: true,
verifiedClosingIssues: [{ issueNumber: 12, title: 'Tracked issue' }],
invalidClosingIssues: [],
});
assert.equal(report.readyForReview, false);
assert.equal(report.needsAuthorFeedback, true);
assert.deepEqual(report.authorActions, [
'Resolve the merge conflicts with the target branch.',
]);
assert.match(renderIntakeComment(report), /Resolve the merge conflicts/);
});
test('unknown mergeability holds readiness even when nothing else is flagged', () => {
const report = buildIntakeReport({
changedPaths: ['doc/devdocs/core/architecture.md'],
body: 'Closes #12',
repositoryHtmlUrl: 'https://github.com/microsoft/PowerToys',
baseRef: 'main',
authorLogin: 'alice',
mergeabilityKnown: false,
verifiedClosingIssues: [{ issueNumber: 12, title: 'Tracked issue' }],
invalidClosingIssues: [],
});
assert.equal(report.readyForReview, false);
assert.equal(report.needsAuthorFeedback, false);
assert.deepEqual(report.authorActions, []);
});
test('product UI paths require visual evidence when none is present', () => {
const report = buildIntakeReport({
changedPaths: ['src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml'],
body: 'Closes #12',
repositoryHtmlUrl: 'https://github.com/microsoft/PowerToys',
baseRef: 'main',
authorLogin: 'alice',
verifiedClosingIssues: [{ issueNumber: 12, title: 'Tracked issue' }],
invalidClosingIssues: [],
});
assert.equal(report.requiresVisualEvidence, true);
assert.equal(report.visualAssessment.visualEvidenceRequirement, 'REQUIRED');
assert.equal(report.readyForReview, false);
assert.match(report.authorActions.join(' '), /screenshot, GIF, or video/);
});
test('product UI paths are satisfied when visual evidence is present', () => {
const report = buildIntakeReport({
changedPaths: ['src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml'],
body: 'Closes #12\n![screenshot](https://example.com/shot.png)',
repositoryHtmlUrl: 'https://github.com/microsoft/PowerToys',
baseRef: 'main',
authorLogin: 'alice',
verifiedClosingIssues: [{ issueNumber: 12, title: 'Tracked issue' }],
invalidClosingIssues: [],
});
assert.equal(report.requiresVisualEvidence, true);
assert.equal(report.readyForReview, true);
assert.doesNotMatch(report.authorActions.join(' '), /screenshot/i);
});
test('draft PR remains incomplete until marked ready', () => {
const report = buildIntakeReport({
changedPaths: ['doc/devdocs/core/architecture.md'],
body: 'Closes #12',
repositoryHtmlUrl: 'https://github.com/microsoft/PowerToys',
baseRef: 'main',
authorLogin: 'alice',
isDraft: true,
verifiedClosingIssues: [{ issueNumber: 12, title: 'Tracked issue' }],
invalidClosingIssues: [],
});
assert.equal(report.readyForReview, false);
assert.equal(report.needsAuthorFeedback, false);
assert.deepEqual(report.authorActions, ['Mark the pull request as ready for review.']);
});
test('readiness and author-feedback labels are mutually managed', () => {
assert.deepEqual(
planManagedLabelChanges(
['Product-Keyboard Manager'],
[READY_FOR_REVIEW_LABEL],
[READY_FOR_REVIEW_LABEL, NEEDS_AUTHOR_FEEDBACK_LABEL],
),
{ add: [READY_FOR_REVIEW_LABEL], remove: [] },
);
assert.deepEqual(
planManagedLabelChanges(
['Product-Keyboard Manager', READY_FOR_REVIEW_LABEL],
[],
[READY_FOR_REVIEW_LABEL, NEEDS_AUTHOR_FEEDBACK_LABEL],
),
{ add: [], remove: [READY_FOR_REVIEW_LABEL] },
);
assert.deepEqual(
planManagedLabelChanges(
['Product-FancyZones', READY_FOR_REVIEW_LABEL],
[NEEDS_AUTHOR_FEEDBACK_LABEL],
[READY_FOR_REVIEW_LABEL, NEEDS_AUTHOR_FEEDBACK_LABEL],
),
{ add: [NEEDS_AUTHOR_FEEDBACK_LABEL], remove: [READY_FOR_REVIEW_LABEL] },
);
});
test('author activity resets the feedback window while bot activity does not', () => {
const existingBody = renderIntakeComment(
{
readyForReview: false,
needsAuthorFeedback: true,
authorLogin: 'alice',
authorActions: ['Update the PR.'],
recommendations: [],
contributingUrl: 'https://example.test/CONTRIBUTING.md',
visualEvidence: { found: false, types: [] },
visualAssessment: {
visualEvidenceRequirement: 'NOT_NEEDED',
visualEvidenceReason: 'The change is nonvisual.',
},
},
'2026-08-01T10:00:00.000Z',
);
assert.equal(
determineFeedbackSince({
needsAuthorFeedback: true,
existingCommentBody: existingBody,
action: 'edited',
senderLogin: 'alice',
authorLogin: 'alice',
now: '2026-08-05T10:00:00.000Z',
}),
'2026-08-05T10:00:00.000Z',
);
assert.equal(
determineFeedbackSince({
needsAuthorFeedback: true,
existingCommentBody: existingBody,
action: 'edited',
senderLogin: ACTIONS_BOT_LOGIN,
authorLogin: 'alice',
now: '2026-08-05T10:00:00.000Z',
}),
'2026-08-01T10:00:00.000Z',
);
});
test('canonical selection trusts only the GitHub Actions bot and chooses the oldest marker', () => {
const body = `${CANONICAL_MARKER}\ncomment`;
const selected = selectCanonicalComment([
botComment(30, body),
{
id: 10,
body,
user: { login: ACTIONS_BOT_LOGIN, id: 99, type: 'Bot' },
},
{
id: 5,
body,
user: { login: 'attacker', id: 1, type: 'User' },
},
botComment(20, body),
]);
assert.equal(selected.canonical.id, 20);
assert.deepEqual(selected.extras.map((entry) => entry.id), [30]);
});
test('canonical upsert updates the oldest trusted comment and deletes extras', async () => {
const body = `${CANONICAL_MARKER}\nfirst`;
const api = new MockApi({
comments: [botComment(20, body), botComment(40, body)],
});
const result = await upsertCanonicalComment({
api,
issueNumber: 12,
body: `${CANONICAL_MARKER}\nupdated`,
});
assert.equal(result.operation, 'updated');
assert.equal(result.comment.id, 20);
assert.deepEqual(result.deletedExtraComments, [40]);
assert.equal(api.created, 0);
assert.equal(api.updated, 1);
assert.deepEqual(api.deleted, [40]);
assert.equal(api.comments.length, 1);
});
test('all-clear comment carries the canonical marker', () => {
const body = renderAllClearComment();
assert.match(body, /^<!-- powertoys-pr-intake:canonical:v1 -->/);
assert.match(body, /All automated intake checks now pass/);
});
function intakeEvent(overrides = {}) {
return {
action: 'opened',
repository: {
full_name: 'microsoft/PowerToys',
html_url: 'https://github.com/microsoft/PowerToys',
},
pull_request: { number: 100 },
sender: { login: 'alice' },
...overrides,
};
}
test('runPullRequestIntake stays silent on a clean PR with no prior comment', async () => {
const api = new MockApi({
issues: [{ number: 100, labels: [], title: 'PR' }],
pullRequests: [{
number: 100,
draft: false,
mergeable: true,
mergeable_state: 'clean',
body: 'Closes #12',
base: { ref: 'main' },
user: { login: 'alice' },
}],
files: [{ filename: 'doc/devdocs/core/architecture.md', status: 'modified' }],
});
api.issues.push({ number: 12, title: 'Tracked issue' });
const result = await runPullRequestIntake({ api, event: intakeEvent() });
assert.equal(result.commentResult.operation, 'skipped');
assert.equal(api.created, 0);
assert.equal(api.updated, 0);
assert.deepEqual(result.labelPlan.add, [READY_FOR_REVIEW_LABEL]);
});
test('runPullRequestIntake replaces a stale comment with an all-clear note when the PR is clean', async () => {
const existing = botComment(50, `${CANONICAL_MARKER}\n## 🧭 PR intake\nplease update`);
const api = new MockApi({
comments: [existing],
issues: [{ number: 100, labels: [NEEDS_AUTHOR_FEEDBACK_LABEL], title: 'PR' }],
pullRequests: [{
number: 100,
draft: false,
mergeable: true,
mergeable_state: 'clean',
body: 'Closes #12',
base: { ref: 'main' },
user: { login: 'alice' },
}],
files: [{ filename: 'doc/devdocs/core/architecture.md', status: 'modified' }],
});
api.issues.push({ number: 12, title: 'Tracked issue' });
const result = await runPullRequestIntake({ api, event: intakeEvent() });
assert.equal(result.commentResult.operation, 'updated');
assert.match(existing.body, /All automated intake checks now pass/);
assert.deepEqual(result.labelPlan.remove, [NEEDS_AUTHOR_FEEDBACK_LABEL]);
});
test('runPullRequestIntake posts a feedback comment when there are remarks', async () => {
const api = new MockApi({
issues: [{ number: 100, labels: [], title: 'PR' }],
pullRequests: [{
number: 100,
draft: false,
mergeable: true,
mergeable_state: 'clean',
body: 'No linked issue here.',
base: { ref: 'main' },
user: { login: 'alice' },
}],
files: [{
filename: 'src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml',
status: 'modified',
}],
});
const result = await runPullRequestIntake({ api, event: intakeEvent() });
assert.equal(result.commentResult.operation, 'created');
assert.equal(api.created, 1);
assert.equal(result.readyForReview, false);
assert.match(api.comments[0].body, /screenshot, GIF, or video/);
});