This is an automated email from the ASF dual-hosted git repository.
damccorm pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git
The following commit(s) were added to refs/heads/master by this push:
new 72d387f3c7e [PR Bot] Select single main and backup reviewer using
label priority (#40315)
72d387f3c7e is described below
commit 72d387f3c7ed78e18c0e0b8487eaa7e91eaccce8
Author: Danny McCormick <[email protected]>
AuthorDate: Tue Oct 13 14:31:56 2026 +0000
[PR Bot] Select single main and backup reviewer using label priority
(#40315)
* [PR Bot] Select single main and backup reviewer using label priority
* Choose randomly among tied highest-priority labels
---
.github/REVIEWERS.yml | 12 ++++
scripts/ci/pr-bot/findPrsNeedingAttention.ts | 17 +++++-
scripts/ci/pr-bot/processNewPrs.ts | 14 ++++-
scripts/ci/pr-bot/shared/commentStrings.ts | 9 +++
scripts/ci/pr-bot/shared/reviewerConfig.ts | 44 ++++++++++----
scripts/ci/pr-bot/shared/reviewersForLabel.ts | 49 ++++++++++++---
scripts/ci/pr-bot/shared/userCommand.ts | 28 ++++++++-
scripts/ci/pr-bot/test/commentStringsTest.ts | 13 ++++
scripts/ci/pr-bot/test/reviewerConfigTest.ts | 79 +++++++++++++++----------
scripts/ci/pr-bot/test/reviewersForLabelTest.ts | 46 ++++++++++++++
10 files changed, 254 insertions(+), 57 deletions(-)
diff --git a/.github/REVIEWERS.yml b/.github/REVIEWERS.yml
index f3c27bec3cf..90034a624f1 100644
--- a/.github/REVIEWERS.yml
+++ b/.github/REVIEWERS.yml
@@ -17,14 +17,18 @@
# add yourself to that label's exclusionList
# FallbackReviewers is for reviewers who can review any area of the code base
that might
# not receive a label. These should generally be more experienced committers.
+# Priority determines which label is used to select a reviewer when a PR has
multiple labels
+# (lower number = higher priority).
labels:
- name: Go
+ priority: 2
reviewers:
- jrmccluskey
- lostluck
- shunping
exclusionList: []
- name: Python
+ priority: 2
reviewers:
- claudevdm
- damccorm
@@ -33,6 +37,7 @@ labels:
- shunping
exclusionList: []
- name: Java
+ priority: 2
reviewers:
- Abacn
- ahmedabu98
@@ -41,28 +46,34 @@ labels:
- kennknowles
exclusionList: []
- name: spanner
+ priority: 1
reviewers:
- nielm
- name: bigtable
+ priority: 1
reviewers:
- igorbernstein2
- mutianf
exclusionList: []
- name: delta
+ priority: 1
reviewers:
- ahmedabu98
- chamikaramj
exclusionList: []
- name: iceberg
+ priority: 1
reviewers:
- ahmedabu98
- chamikaramj
exclusionList: []
- name: yaml
+ priority: 1
reviewers:
- chamikaramj
- derrickaw
- name: Build
+ priority: 3
reviewers:
- Abacn
- Amar3tto
@@ -70,6 +81,7 @@ labels:
- derrickaw
exclusionList: []
- name: website
+ priority: 3
reviewers:
- Abacn
- damccorm
diff --git a/scripts/ci/pr-bot/findPrsNeedingAttention.ts
b/scripts/ci/pr-bot/findPrsNeedingAttention.ts
index 4b1d97201f9..829a69d6d59 100644
--- a/scripts/ci/pr-bot/findPrsNeedingAttention.ts
+++ b/scripts/ci/pr-bot/findPrsNeedingAttention.ts
@@ -112,9 +112,11 @@ async function assignToNewReviewers(
reviewersToExclude.push(pull.user.login);
const reviewersForLabels: { [key: string]: string[] } =
reviewerConfig.getReviewersForLabels(labelObjects, reviewersToExclude);
- const fallbackReviewers = reviewerConfig.getFallbackReviewers();
- for (const labelObject of labelObjects) {
- const label = labelObject.name;
+ const fallbackReviewers =
+ reviewerConfig.getFallbackReviewers(reviewersToExclude);
+ let backupReviewersForLabels: { [key: string]: string } = {};
+ prState.reviewersAssignedForLabels = {};
+ for (const label of Object.keys(reviewersForLabels)) {
let availableReviewers = reviewersForLabels[label];
if (availableReviewers && availableReviewers.length > 0) {
let reviewersState = await stateClient.getReviewersForLabelState(label);
@@ -122,8 +124,16 @@ async function assignToNewReviewers(
availableReviewers,
fallbackReviewers
);
+ let backupReviewer = reviewersState.getBackupReviewer(
+ availableReviewers,
+ chosenReviewer,
+ fallbackReviewers
+ );
reviewerStateToUpdate[label] = reviewersState;
prState.reviewersAssignedForLabels[label] = chosenReviewer;
+ if (backupReviewer) {
+ backupReviewersForLabels[label] = backupReviewer;
+ }
}
}
@@ -132,6 +142,7 @@ async function assignToNewReviewers(
pull.number,
commentStrings.assignNewReviewer(prState.reviewersAssignedForLabels, {
labels: pull.labels,
+ backupReviewers: backupReviewersForLabels,
})
);
await github.requestPrReviewers(
diff --git a/scripts/ci/pr-bot/processNewPrs.ts
b/scripts/ci/pr-bot/processNewPrs.ts
index 2f24f8e08f4..f027b75f4fa 100644
--- a/scripts/ci/pr-bot/processNewPrs.ts
+++ b/scripts/ci/pr-bot/processNewPrs.ts
@@ -344,8 +344,11 @@ export async function processPull(
// Pick reviewers to assign. Store them in reviewerStateToUpdate and update
the prState object with those reviewers (and their associated labels)
let reviewerStateToUpdate: { [key: string]: typeof ReviewersForLabel } = {};
+ let backupReviewersForLabels: { [key: string]: string } = {};
+ const exclusionList = [pull.user.login];
const reviewersForLabels: { [key: string]: string[] } =
- reviewerConfig.getReviewersForLabels(pull.labels, [pull.user.login]);
+ reviewerConfig.getReviewersForLabels(pull.labels, exclusionList);
+ const fallbackReviewers = reviewerConfig.getFallbackReviewers(exclusionList);
var labels = Object.keys(reviewersForLabels);
if (!labels || labels.length === 0) {
return;
@@ -354,8 +357,16 @@ export async function processPull(
let availableReviewers = reviewersForLabels[label];
let reviewersState = await stateClient.getReviewersForLabelState(label);
let chosenReviewer = reviewersState.assignNextReviewer(availableReviewers);
+ let backupReviewer = reviewersState.getBackupReviewer(
+ availableReviewers,
+ chosenReviewer,
+ fallbackReviewers
+ );
reviewerStateToUpdate[label] = reviewersState;
prState.reviewersAssignedForLabels[label] = chosenReviewer;
+ if (backupReviewer) {
+ backupReviewersForLabels[label] = backupReviewer;
+ }
}
console.log(`Assigning reviewers for PR ${pull.number}`);
@@ -363,6 +374,7 @@ export async function processPull(
pull.number,
commentStrings.assignReviewer(prState.reviewersAssignedForLabels, {
labels: pull.labels,
+ backupReviewers: backupReviewersForLabels,
})
);
await github.requestPrReviewers(
diff --git a/scripts/ci/pr-bot/shared/commentStrings.ts
b/scripts/ci/pr-bot/shared/commentStrings.ts
index f1a39be4c55..edf16d27b83 100644
--- a/scripts/ci/pr-bot/shared/commentStrings.ts
+++ b/scripts/ci/pr-bot/shared/commentStrings.ts
@@ -22,6 +22,7 @@ const { NO_MATCHING_LABEL } = require("./constants");
export interface AssignReviewerOptions {
labels?: (string | Label)[];
notices?: string[];
+ backupReviewers?: { [label: string]: string };
}
// Custom notices for specific labels
@@ -63,11 +64,15 @@ export function assignReviewer(
for (let label in labelToReviewerMapping) {
let reviewer = labelToReviewerMapping[label];
+ let backupReviewer = options?.backupReviewers?.[label];
if (label === NO_MATCHING_LABEL) {
commentString += `R: @${reviewer} added as fallback since no labels
match configuration\n`;
} else {
commentString += `R: @${reviewer} for label ${label}.\n`;
}
+ if (backupReviewer) {
+ commentString += `Backup reviewer: ${backupReviewer} (if the main
reviewer is unresponsive, you can escalate to them by tagging their
username)\n`;
+ }
}
commentString += formatNotices(labelToReviewerMapping, options);
@@ -158,11 +163,15 @@ export function assignNewReviewer(
for (const label in labelToReviewerMapping) {
const reviewer = labelToReviewerMapping[label];
+ const backupReviewer = options?.backupReviewers?.[label];
if (label === NO_MATCHING_LABEL) {
commentString += `R: @${reviewer} added as fallback since no labels
match configuration\n`;
} else {
commentString += `R: @${reviewer} for label ${label}.\n`;
}
+ if (backupReviewer) {
+ commentString += `Backup reviewer: ${backupReviewer} (if the main
reviewer is unresponsive, you can escalate to them by tagging their
username)\n`;
+ }
}
commentString += formatNotices(labelToReviewerMapping, options);
diff --git a/scripts/ci/pr-bot/shared/reviewerConfig.ts
b/scripts/ci/pr-bot/shared/reviewerConfig.ts
index 8fc0d32b014..8feb7c2df6e 100644
--- a/scripts/ci/pr-bot/shared/reviewerConfig.ts
+++ b/scripts/ci/pr-bot/shared/reviewerConfig.ts
@@ -44,30 +44,52 @@ export class ReviewerConfig {
}
// Given a list of labels and an exclusion list of reviewers not to include
(e.g. the author)
- // returns all possible reviewers for each label
+ // returns all possible reviewers for the single highest-priority matching
label
+ // (lower priority number = higher priority; ties broken randomly).
getReviewersForLabels(
labels: Label[],
exclusionList: string[]
): { [key: string]: string[] } {
- let reviewersFound = false;
- let labelToReviewerMapping = {};
- labels.forEach((label) => {
+ let labelToReviewerMapping: { [key: string]: string[] } = {};
+ let bestPriority: number = Number.MAX_SAFE_INTEGER;
+ let bestMatches: { label: string; reviewers: string[] }[] = [];
+
+ for (const label of labels || []) {
let reviewers = this.getReviewersForLabel(label.name, exclusionList);
if (reviewers.length > 0) {
- labelToReviewerMapping[label.name] = reviewers;
- reviewersFound = true;
+ const priority = this.getPriorityForLabel(label.name);
+ if (bestMatches.length === 0 || priority < bestPriority) {
+ bestPriority = priority;
+ bestMatches = [{ label: label.name, reviewers }];
+ } else if (priority === bestPriority) {
+ bestMatches.push({ label: label.name, reviewers });
+ }
}
- });
- if (!reviewersFound) {
+ }
+ if (bestMatches.length > 0) {
+ const chosenMatch =
+ bestMatches[Math.floor(Math.random() * bestMatches.length)];
+ labelToReviewerMapping[chosenMatch.label] = chosenMatch.reviewers;
+ } else {
const fallbackReviewers = this.getFallbackReviewers(exclusionList);
if (fallbackReviewers.length > 0) {
- labelToReviewerMapping[NO_MATCHING_LABEL] =
- this.getFallbackReviewers(exclusionList);
+ labelToReviewerMapping[NO_MATCHING_LABEL] = fallbackReviewers;
}
}
return labelToReviewerMapping;
}
+ getPriorityForLabel(label: string): number {
+ const labelObjects = this.config.labels;
+ const labelObject = labelObjects.find(
+ (labelObject) => labelObject.name.toLowerCase() === label.toLowerCase()
+ );
+ if (!labelObject || typeof labelObject.priority !== "number") {
+ return Number.MAX_SAFE_INTEGER;
+ }
+ return labelObject.priority;
+ }
+
// Get possible reviewers excluding the author.
getReviewersForLabel(label: string, exclusionList: string[]): string[] {
const labelObjects = this.config.labels;
@@ -106,6 +128,8 @@ export class ReviewerConfig {
# add yourself to that label's exclusionList
# FallbackReviewers is for reviewers who can review any area of the code base
that might
# not receive a label. These should generally be more experienced committers.
+# Priority determines which label is used to select a reviewer when a PR has
multiple labels
+# (lower number = higher priority).
${yaml.dump(this.config)}`;
fs.writeFileSync(this.configPath, contents, { encoding: "utf-8" });
diff --git a/scripts/ci/pr-bot/shared/reviewersForLabel.ts
b/scripts/ci/pr-bot/shared/reviewersForLabel.ts
index f46f5359a8b..40b0a6c2a57 100644
--- a/scripts/ci/pr-bot/shared/reviewersForLabel.ts
+++ b/scripts/ci/pr-bot/shared/reviewersForLabel.ts
@@ -40,16 +40,14 @@ export class ReviewersForLabel {
}
}
- // Given a list of available reviewers,
- // returns the next reviewer up based on who has reviewed least recently.
- // Updates this object to reflect their assignment.
- assignNextReviewer(availableReviewers: string[]): string {
- if (availableReviewers.length === 0) {
- throw new Error(`No reviewers available for label ${this.label}`);
+ private findLeastRecentlyAssignedReviewer(
+ availableReviewers: string[]
+ ): string {
+ if (!availableReviewers || availableReviewers.length === 0) {
+ return "";
}
if (!this.dateOfLastReviewAssignment[availableReviewers[0]]) {
- this.dateOfLastReviewAssignment[availableReviewers[0]] = Date.now();
return availableReviewers[0];
}
@@ -59,7 +57,6 @@ export class ReviewersForLabel {
for (let i = 0; i < availableReviewers.length; i++) {
let availableReviewer = availableReviewers[i];
if (!this.dateOfLastReviewAssignment[availableReviewer]) {
- this.dateOfLastReviewAssignment[availableReviewer] = Date.now();
return availableReviewer;
}
if (earliestDate > this.dateOfLastReviewAssignment[availableReviewer]) {
@@ -68,10 +65,44 @@ export class ReviewersForLabel {
}
}
- this.dateOfLastReviewAssignment[earliestReviewer] = Date.now();
return earliestReviewer;
}
+ // Given a list of available reviewers,
+ // returns the next reviewer up based on who has reviewed least recently.
+ // Updates this object to reflect their assignment.
+ assignNextReviewer(availableReviewers: string[]): string {
+ if (availableReviewers.length === 0) {
+ throw new Error(`No reviewers available for label ${this.label}`);
+ }
+
+ const chosenReviewer =
+ this.findLeastRecentlyAssignedReviewer(availableReviewers);
+ this.dateOfLastReviewAssignment[chosenReviewer] = Date.now();
+ return chosenReviewer;
+ }
+
+ // Returns a backup reviewer from the same label (excluding the main
reviewer)
+ // without updating dateOfLastReviewAssignment. Falls back to
fallbackReviewers
+ // if the label has no other eligible reviewers.
+ getBackupReviewer(
+ availableReviewers: string[],
+ mainReviewer: string,
+ fallbackReviewers: string[] = []
+ ): string {
+ const remainingLabelReviewers = (availableReviewers || []).filter(
+ (reviewer) => reviewer !== mainReviewer
+ );
+ if (remainingLabelReviewers.length > 0) {
+ return this.findLeastRecentlyAssignedReviewer(remainingLabelReviewers);
+ }
+
+ const remainingFallbackReviewers = (fallbackReviewers || []).filter(
+ (reviewer) => reviewer !== mainReviewer
+ );
+ return this.findLeastRecentlyAssignedReviewer(remainingFallbackReviewers);
+ }
+
// Given the up to date list of available reviewers (excluding the author),
// returns the next reviewer up based on who has reviewed least recently.
// Updates this object to reflect their assignment.
diff --git a/scripts/ci/pr-bot/shared/userCommand.ts
b/scripts/ci/pr-bot/shared/userCommand.ts
index 8c09013553e..79852c475f4 100644
--- a/scripts/ci/pr-bot/shared/userCommand.ts
+++ b/scripts/ci/pr-bot/shared/userCommand.ts
@@ -92,12 +92,23 @@ async function assignToNextReviewer(
labelOfReviewer
);
const pullAuthor = github.getPullAuthorFromPayload(payload);
+ const exclusionList = [commentAuthor, pullAuthor];
let availableReviewers = reviewerConfig.getReviewersForLabel(
labelOfReviewer,
- [commentAuthor, pullAuthor]
+ exclusionList
);
+ const fallbackReviewers =
+ reviewerConfig.getFallbackReviewers(exclusionList);
let chosenReviewer = reviewersState.assignNextReviewer(availableReviewers);
+ let backupReviewer = reviewersState.getBackupReviewer(
+ availableReviewers,
+ chosenReviewer,
+ fallbackReviewers
+ );
prState.reviewersAssignedForLabels[labelOfReviewer] = chosenReviewer;
+ const backupReviewersForLabels: { [key: string]: string } = backupReviewer
+ ? { [labelOfReviewer]: backupReviewer }
+ : {};
// Comment assigning reviewer
console.log(`Assigning ${chosenReviewer}`);
@@ -107,6 +118,7 @@ async function assignToNextReviewer(
pullNumber,
commentStrings.assignReviewer(prState.reviewersAssignedForLabels, {
labels: existingLabels,
+ backupReviewers: backupReviewersForLabels,
})
);
await github.requestPrReviewers(pullNumber, [chosenReviewer]);
@@ -206,11 +218,14 @@ async function assignReviewerSet(
const existingLabels = payload.issue?.labels || payload.pull_request?.labels;
const pullAuthor = github.getPullAuthorFromPayload(payload);
+ const exclusionList = [pullAuthor];
const reviewersForLabels = reviewerConfig.getReviewersForLabels(
existingLabels,
- [pullAuthor]
+ exclusionList
);
+ const fallbackReviewers = reviewerConfig.getFallbackReviewers(exclusionList);
let reviewerStateToUpdate = {};
+ let backupReviewersForLabels: { [key: string]: string } = {};
var labels = Object.keys(reviewersForLabels);
if (!labels || labels.length == 0) {
await github.addPrComment(
@@ -224,14 +239,23 @@ async function assignReviewerSet(
let availableReviewers = reviewersForLabels[label];
let reviewersState = await stateClient.getReviewersForLabelState(label);
let chosenReviewer = reviewersState.assignNextReviewer(availableReviewers);
+ let backupReviewer = reviewersState.getBackupReviewer(
+ availableReviewers,
+ chosenReviewer,
+ fallbackReviewers
+ );
reviewerStateToUpdate[label] = reviewersState;
prState.reviewersAssignedForLabels[label] = chosenReviewer;
+ if (backupReviewer) {
+ backupReviewersForLabels[label] = backupReviewer;
+ }
}
console.log(`Assigning reviewers for pr ${pullNumber}`);
await github.addPrComment(
pullNumber,
commentStrings.assignReviewer(prState.reviewersAssignedForLabels, {
labels: existingLabels,
+ backupReviewers: backupReviewersForLabels,
})
);
await github.requestPrReviewers(
diff --git a/scripts/ci/pr-bot/test/commentStringsTest.ts
b/scripts/ci/pr-bot/test/commentStringsTest.ts
index f2a16ca1c63..62456fd4994 100644
--- a/scripts/ci/pr-bot/test/commentStringsTest.ts
+++ b/scripts/ci/pr-bot/test/commentStringsTest.ts
@@ -41,5 +41,18 @@ describe("commentStrings", function () {
const comment = commentStrings.assignReviewer({ core: "reviewer1" });
assert(comment.includes("review with scrutiny"));
});
+
+ it("should mention backup reviewer without tagging them with @", function
() {
+ const comment = commentStrings.assignReviewer(
+ { Java: "mainReviewer" },
+ {
+ labels: [{ name: "Java" }],
+ backupReviewers: { Java: "backupUser" },
+ }
+ );
+ assert(comment.includes("R: @mainReviewer for label Java."));
+ assert(comment.includes("Backup reviewer: backupUser"));
+ assert(!comment.includes("@backupUser"));
+ });
});
});
diff --git a/scripts/ci/pr-bot/test/reviewerConfigTest.ts
b/scripts/ci/pr-bot/test/reviewerConfigTest.ts
index 589aab635bc..95c39188824 100644
--- a/scripts/ci/pr-bot/test/reviewerConfigTest.ts
+++ b/scripts/ci/pr-bot/test/reviewerConfigTest.ts
@@ -22,15 +22,22 @@ const { ReviewerConfig } =
require("../shared/reviewerConfig");
const configPath = "test-config.yml";
const configContents = `labels:
- name: "Go"
+ priority: 2
reviewers: ["testReviewer1", "testReviewer2"]
exclusionList: ["testReviewer3"] # These users will never be suggested as
reviewers
# I don't know the other areas well enough to assess who the normal
committers/contributors who might want to be reviewers are
- name: "Java"
+ priority: 3
reviewers: ["testReviewer3", "testReviewer2"]
exclusionList: [] # These users will never be suggested as reviewers
- name: "Python"
+ priority: 1
reviewers: ["testReviewer4"]
exclusionList: [] # These users will never be suggested as reviewers
+- name: "Kafka"
+ priority: 1
+ reviewers: ["testReviewer5"]
+ exclusionList: []
fallbackReviewers: ["testReviewer5", "testReviewer1", "testReviewer3"] # List
of committers to use when no label matches
`;
describe("ReviewerConfig", function () {
@@ -46,12 +53,13 @@ describe("ReviewerConfig", function () {
});
describe("getReviewersForLabels()", function () {
- it("should return all reviewers configured for all labels", function () {
+ it("should return reviewers only for the highest-priority matching label",
function () {
const config = new ReviewerConfig(configPath);
const reviewersForLabels = config.getReviewersForLabels(
- [{ name: "Go" }, { name: "Java" }],
+ [{ name: "Java" }, { name: "Go" }],
[]
);
+ assert.deepEqual(Object.keys(reviewersForLabels), ["Go"]);
assert(
reviewersForLabels["Go"].find(
(reviewer) => reviewer === "testReviewer1"
@@ -76,44 +84,51 @@ describe("ReviewerConfig", function () {
),
"Return value for Go label should not include testReviewer4"
);
-
- assert(
- reviewersForLabels["Java"].find(
- (reviewer) => reviewer === "testReviewer3"
- ),
- "Return value for Java label should include testReviewer3"
- );
- assert(
- reviewersForLabels["Java"].find(
- (reviewer) => reviewer === "testReviewer2"
- ),
- "Return value for Java label should include testReviewer2"
- );
- assert(
- !reviewersForLabels["Java"].find(
- (reviewer) => reviewer === "testReviewer4"
- ),
- "Return value for Java label should not include testReviewer4"
- );
- assert(
- !reviewersForLabels["Java"].find(
- (reviewer) => reviewer === "testReviewer1"
- ),
- "Return value for Java label should not include testReviewer1"
- );
assert(
- !reviewersForLabels["Java"].find(
- (reviewer) => reviewer === "testReviewer5"
- ),
- "Return value for Java label should not include testReviewer5"
+ Object.keys(reviewersForLabels).indexOf("Java") == -1,
+ "Lower-priority Java label should not be included when Go matches"
);
-
assert(
Object.keys(reviewersForLabels).indexOf("Python") == -1,
"No reviewers should be included for python"
);
});
+ it("should choose randomly when multiple labels share the highest
priority", function () {
+ const config = new ReviewerConfig(configPath);
+ const origRandom = Math.random;
+ try {
+ Math.random = () => 0.1;
+ const firstChoice = config.getReviewersForLabels(
+ [{ name: "Python" }, { name: "Kafka" }, { name: "Java" }],
+ []
+ );
+ assert.deepEqual(Object.keys(firstChoice), ["Python"]);
+
+ Math.random = () => 0.9;
+ const secondChoice = config.getReviewersForLabels(
+ [{ name: "Python" }, { name: "Kafka" }, { name: "Java" }],
+ []
+ );
+ assert.deepEqual(Object.keys(secondChoice), ["Kafka"]);
+ } finally {
+ Math.random = origRandom;
+ }
+ });
+
+ it("should fall back to next highest-priority label if top priority label
has all reviewers excluded", function () {
+ const config = new ReviewerConfig(configPath);
+ const reviewersForLabels = config.getReviewersForLabels(
+ [{ name: "Python" }, { name: "Java" }],
+ ["testReviewer4"]
+ );
+ assert.deepEqual(Object.keys(reviewersForLabels), ["Java"]);
+ assert.deepEqual(reviewersForLabels["Java"], [
+ "testReviewer3",
+ "testReviewer2",
+ ]);
+ });
+
it("should return no entry if a label is not configured", function () {
const config = new ReviewerConfig(configPath);
const reviewersForLabels = config.getReviewersForLabels(
diff --git a/scripts/ci/pr-bot/test/reviewersForLabelTest.ts
b/scripts/ci/pr-bot/test/reviewersForLabelTest.ts
index f4257505059..899b0519c1f 100644
--- a/scripts/ci/pr-bot/test/reviewersForLabelTest.ts
+++ b/scripts/ci/pr-bot/test/reviewersForLabelTest.ts
@@ -132,4 +132,50 @@ describe("ReviewersForLabel", function () {
assert.throws(() => reviewersForGo.assignNextReviewer([]));
});
});
+
+ describe("getBackupReviewer()", function () {
+ it("should pick the next least recently assigned reviewer from the label
without updating timestamps", function () {
+ const dateOfLastReviewAssignment = {
+ testReviewer1: 10,
+ testReviewer2: 20,
+ testReviewer3: 30,
+ };
+ let reviewersForGo = new ReviewersForLabel("Go", {
+ dateOfLastReviewAssignment: { ...dateOfLastReviewAssignment },
+ });
+
+ const backup = reviewersForGo.getBackupReviewer(
+ ["testReviewer1", "testReviewer2", "testReviewer3"],
+ "testReviewer1",
+ ["fallback1"]
+ );
+ assert.equal(backup, "testReviewer2");
+ assert.deepEqual(
+ reviewersForGo.dateOfLastReviewAssignment,
+ dateOfLastReviewAssignment
+ );
+ });
+
+ it("should fall back to fallbackReviewers when label only has one
available reviewer", function () {
+ const dateOfLastReviewAssignment = {
+ testReviewer1: 10,
+ fallback1: 50,
+ fallback2: 15,
+ };
+ let reviewersForSpanner = new ReviewersForLabel("spanner", {
+ dateOfLastReviewAssignment: { ...dateOfLastReviewAssignment },
+ });
+
+ const backup = reviewersForSpanner.getBackupReviewer(
+ ["testReviewer1"],
+ "testReviewer1",
+ ["testReviewer1", "fallback1", "fallback2"]
+ );
+ assert.equal(backup, "fallback2");
+ assert.deepEqual(
+ reviewersForSpanner.dateOfLastReviewAssignment,
+ dateOfLastReviewAssignment
+ );
+ });
+ });
});