Move status and revert loading from change view into models

Release-Notes: skip
Google-Bug-Id: b/247042673
Change-Id: I1f372b2ae62ba8fa1d978c726c29ede8754a5d2f
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index 6892549..00e9550 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -87,7 +87,6 @@
   ServerInfo,
   UrlEncodedCommentId,
   isRobot,
-  ChangeStates,
 } from '../../../types/common';
 import {FocusTarget, GrReplyDialog} from '../gr-reply-dialog/gr-reply-dialog';
 import {GrIncludedInDialog} from '../gr-included-in-dialog/gr-included-in-dialog';
@@ -130,7 +129,6 @@
   until,
 } from '../../../utils/async-util';
 import {Interaction, Timing} from '../../../constants/reporting';
-import {getRevertCreatedChangeIds} from '../../../utils/message-util';
 import {
   getAddedByReason,
   getRemovedByReason,
@@ -146,7 +144,7 @@
 import {resolve} from '../../../models/dependency';
 import {checksModelToken} from '../../../models/checks/checks-model';
 import {changeModelToken} from '../../../models/change/change-model';
-import {css, html, LitElement, nothing, PropertyValues} from 'lit';
+import {css, html, LitElement, nothing} from 'lit';
 import {a11yStyles} from '../../../styles/gr-a11y-styles';
 import {paperStyles} from '../../../styles/gr-paper-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
@@ -170,6 +168,7 @@
 import {userModelToken} from '../../../models/user/user-model';
 import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
 import {modalStyles} from '../../../styles/gr-modal-styles';
+import {relatedChangesModelToken} from '../../../models/change/related-changes-model';
 
 const MIN_LINES_FOR_COMMIT_COLLAPSE = 18;
 
@@ -381,10 +380,6 @@
   @state()
   replyDisabled = true;
 
-  // Private but used in tests.
-  @state()
-  changeStatuses: ChangeStates[] = [];
-
   @state()
   private updateCheckTimerHandle?: number | null;
 
@@ -468,7 +463,7 @@
   private tabState?: TabState;
 
   @state()
-  private revertedChange?: ChangeInfo;
+  private revertingChange?: ChangeInfo;
 
   // Private but used in tests.
   @state()
@@ -499,6 +494,11 @@
 
   private readonly getViewModel = resolve(this, changeViewModelToken);
 
+  private readonly getRelatedChangesModel = resolve(
+    this,
+    relatedChangesModelToken
+  );
+
   private readonly getShortcutsService = resolve(this, shortcutsServiceToken);
 
   private scrollTask?: DelayedTask;
@@ -758,6 +758,13 @@
         this.projectConfig = config;
       }
     );
+    subscribe(
+      this,
+      () => this.getRelatedChangesModel().revertingChange$,
+      revertingChange => {
+        this.revertingChange = revertingChange;
+      }
+    );
   }
 
   override connectedCallback() {
@@ -833,18 +840,6 @@
     super.disconnectedCallback();
   }
 
-  protected override willUpdate(changedProperties: PropertyValues): void {
-    if (
-      changedProperties.has('change') ||
-      changedProperties.has('mergeable') ||
-      changedProperties.has('currentRevisionActions')
-    ) {
-      // TODO: Just compute `changeStatuses` on the fly. No need for it to be a
-      // @state object.
-      this.changeStatuses = this.computeChangeStatusChips();
-    }
-  }
-
   static override get styles() {
     return [
       a11yStyles,
@@ -1224,13 +1219,14 @@
   }
 
   private renderHeaderTitle() {
+    const changeStatuses = this.computeChangeStatusChips();
     const resolveWeblinks =
       this.revision?.commit?.resolve_conflicts_web_links ?? [];
     return html` <div class="headerTitle">
       <div class="changeStatuses">
-        ${this.changeStatuses.map(
+        ${changeStatuses.map(
           status => html` <gr-change-status
-            .revertedChange=${this.revertedChange}
+            .revertedChange=${this.revertingChange}
             .status=${status}
             .resolveWeblinks=${resolveWeblinks}
           ></gr-change-status>`
@@ -1358,7 +1354,7 @@
         <gr-change-metadata
           id="metadata"
           .change=${this.change}
-          .revertedChange=${this.revertedChange}
+          .revertedChange=${this.revertingChange}
           .account=${this.account}
           .revision=${this.revision}
           .commitInfo=${this.revision?.commit}
@@ -1761,9 +1757,9 @@
     if (!this.change || this.mergeable === undefined) return [];
 
     const options = {
-      includeDerived: true,
       mergeable: this.mergeable,
       submitEnabled: !!this.isSubmitEnabled(),
+      revertingChangeStatus: this.revertingChange?.status,
     };
     return changeStatuses(this.change as ChangeInfo, options);
   }
@@ -2557,41 +2553,6 @@
     }
   }
 
-  computeRevertSubmitted(change?: ChangeInfo | ParsedChangeInfo) {
-    if (!change?.messages) return;
-    Promise.all(
-      getRevertCreatedChangeIds(change.messages).map(changeId =>
-        this.restApiService.getChange(changeId)
-      )
-    ).then(changes => {
-      // if a change is deleted then getChanges returns null for that changeId
-      changes = changes.filter(
-        change => change && change.status !== ChangeStatus.ABANDONED
-      );
-      if (!changes.length) return;
-      const submittedRevert = changes.find(
-        change => change?.status === ChangeStatus.MERGED
-      );
-      if (!this.changeStatuses) return;
-      // Protect against `computeRevertSubmitted()` being called twice.
-      // TODO: Convert this to be rxjs based, so computeRevertSubmitted() is not
-      // actively called, but instead we can subscribe to something.
-      if (this.changeStatuses.includes(ChangeStates.REVERT_SUBMITTED)) return;
-      if (this.changeStatuses.includes(ChangeStates.REVERT_CREATED)) return;
-      if (submittedRevert) {
-        this.revertedChange = submittedRevert;
-        this.changeStatuses = this.changeStatuses.concat([
-          ChangeStates.REVERT_SUBMITTED,
-        ]);
-      } else {
-        if (changes[0]) this.revertedChange = changes[0];
-        this.changeStatuses = this.changeStatuses.concat([
-          ChangeStates.REVERT_CREATED,
-        ]);
-      }
-    });
-  }
-
   private async untilModelLoaded() {
     // NOTE: Wait until this page is connected before determining whether the
     // model is loaded.  This can happen when viewState changes when setting up
@@ -2632,8 +2593,6 @@
     if (!this.change.reviewer_updates) {
       this.change.reviewer_updates = null as unknown as undefined;
     }
-
-    this.computeRevertSubmitted(this.change);
   }
 
   private isParentCurrent() {
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
index 736b5c5..845ab65 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
@@ -12,7 +12,6 @@
   CommentSide,
   DefaultBase,
   DiffViewMode,
-  MessageTag,
   createDefaultPreferences,
   Tab,
 } from '../../../constants/constants';
@@ -32,7 +31,6 @@
 import {
   createChangeViewState,
   createApproval,
-  createChange,
   createChangeMessages,
   createCommit,
   createPreferences,
@@ -56,7 +54,6 @@
   EDIT,
   NumericChangeId,
   PARENT,
-  ReviewInputTag,
   RevisionPatchSetNum,
   RobotId,
   RobotCommentInfo,
@@ -67,7 +64,6 @@
   QuickLabelInfo,
   PatchSetNumber,
   CommentThread,
-  ChangeStates,
   SavingState,
 } from '../../../types/common';
 import {GrEditControls} from '../../edit/gr-edit-controls/gr-edit-controls';
@@ -1126,183 +1122,6 @@
     );
   });
 
-  test('changeStatuses', async () => {
-    element.loading = false;
-    element.change = {
-      ...createChangeViewChange(),
-      revisions: {
-        rev2: createRevision(2),
-        rev1: createRevision(1),
-        rev13: createRevision(13),
-        rev3: createRevision(3),
-      },
-      current_revision: 'rev3' as CommitId,
-      status: ChangeStatus.MERGED,
-      labels: {
-        test: {
-          all: [],
-          default_value: 0,
-          values: {},
-          approved: {},
-        },
-      },
-    };
-    element.mergeable = true;
-    await element.updateComplete;
-    const expectedStatuses = [ChangeStates.MERGED];
-    assert.deepEqual(element.changeStatuses, expectedStatuses);
-    const statusChips =
-      element.shadowRoot!.querySelectorAll('gr-change-status');
-    assert.equal(statusChips.length, 1);
-  });
-
-  suite('ChangeStatus revert', () => {
-    test('do not show any chip if no revert created', async () => {
-      const change = {
-        ...createParsedChange(),
-        messages: createChangeMessages(2),
-      };
-      const getChangeStub = stubRestApi('getChange');
-      getChangeStub.onFirstCall().returns(
-        Promise.resolve({
-          ...createChange(),
-        })
-      );
-      getChangeStub.onSecondCall().returns(
-        Promise.resolve({
-          ...createChange(),
-        })
-      );
-      element.change = change;
-      element.mergeable = true;
-      element.currentRevisionActions = {submit: {enabled: true}};
-      assert.isTrue(element.isSubmitEnabled());
-      await element.updateComplete;
-      element.computeRevertSubmitted(element.change);
-      await element.updateComplete;
-      assert.isFalse(
-        element.changeStatuses?.includes(ChangeStates.REVERT_SUBMITTED)
-      );
-      assert.isFalse(
-        element.changeStatuses?.includes(ChangeStates.REVERT_CREATED)
-      );
-    });
-
-    test('do not show any chip if all reverts are abandoned', async () => {
-      const change = {
-        ...createParsedChange(),
-        messages: createChangeMessages(2),
-      };
-      change.messages[0].message = 'Created a revert of this change as 12345';
-      change.messages[0].tag = MessageTag.TAG_REVERT as ReviewInputTag;
-
-      change.messages[1].message = 'Created a revert of this change as 23456';
-      change.messages[1].tag = MessageTag.TAG_REVERT as ReviewInputTag;
-
-      const getChangeStub = stubRestApi('getChange');
-      getChangeStub.onFirstCall().returns(
-        Promise.resolve({
-          ...createChange(),
-          status: ChangeStatus.ABANDONED,
-        })
-      );
-      getChangeStub.onSecondCall().returns(
-        Promise.resolve({
-          ...createChange(),
-          status: ChangeStatus.ABANDONED,
-        })
-      );
-      element.change = change;
-      element.mergeable = true;
-      element.currentRevisionActions = {submit: {enabled: true}};
-      assert.isTrue(element.isSubmitEnabled());
-      await element.updateComplete;
-      element.computeRevertSubmitted(element.change);
-      await element.updateComplete;
-      assert.isFalse(
-        element.changeStatuses?.includes(ChangeStates.REVERT_SUBMITTED)
-      );
-      assert.isFalse(
-        element.changeStatuses?.includes(ChangeStates.REVERT_CREATED)
-      );
-    });
-
-    test('show revert created if no revert is merged', async () => {
-      const change = {
-        ...createParsedChange(),
-        messages: createChangeMessages(2),
-      };
-      change.messages[0].message = 'Created a revert of this change as 12345';
-      change.messages[0].tag = MessageTag.TAG_REVERT as ReviewInputTag;
-
-      change.messages[1].message = 'Created a revert of this change as 23456';
-      change.messages[1].tag = MessageTag.TAG_REVERT as ReviewInputTag;
-
-      const getChangeStub = stubRestApi('getChange');
-      getChangeStub.onFirstCall().returns(
-        Promise.resolve({
-          ...createChange(),
-        })
-      );
-      getChangeStub.onSecondCall().returns(
-        Promise.resolve({
-          ...createChange(),
-        })
-      );
-      element.change = change;
-      element.mergeable = true;
-      element.currentRevisionActions = {submit: {enabled: true}};
-      assert.isTrue(element.isSubmitEnabled());
-      await element.updateComplete;
-      element.computeRevertSubmitted(element.change);
-      // Wait for promises to settle.
-      await waitEventLoop();
-      await element.updateComplete;
-      assert.isFalse(
-        element.changeStatuses?.includes(ChangeStates.REVERT_SUBMITTED)
-      );
-      assert.isTrue(
-        element.changeStatuses?.includes(ChangeStates.REVERT_CREATED)
-      );
-    });
-
-    test('show revert submitted if revert is merged', async () => {
-      const change = {
-        ...createParsedChange(),
-        messages: createChangeMessages(2),
-      };
-      change.messages[0].message = 'Created a revert of this change as 12345';
-      change.messages[0].tag = MessageTag.TAG_REVERT as ReviewInputTag;
-      const getChangeStub = stubRestApi('getChange');
-      getChangeStub.onFirstCall().returns(
-        Promise.resolve({
-          ...createChange(),
-          status: ChangeStatus.MERGED,
-        })
-      );
-      getChangeStub.onSecondCall().returns(
-        Promise.resolve({
-          ...createChange(),
-        })
-      );
-      element.change = change;
-      element.mergeable = true;
-      element.currentRevisionActions = {submit: {enabled: true}};
-      assert.isTrue(element.isSubmitEnabled());
-      await element.updateComplete;
-      element.computeRevertSubmitted(element.change);
-      // Wait for promises to settle.
-      await waitEventLoop();
-      await element.updateComplete;
-      assert.isFalse(
-        element.changeStatuses?.includes(ChangeStates.REVERT_CREATED)
-      );
-      assert.isTrue(
-        element.changeStatuses?.includes(ChangeStates.REVERT_SUBMITTED)
-      );
-    });
-  });
-
   test('diff preferences open when open-diff-prefs is fired', async () => {
     await element.updateComplete;
     assertIsDefined(element.fileList);
diff --git a/polygerrit-ui/app/models/change/change-model.ts b/polygerrit-ui/app/models/change/change-model.ts
index 8be2c51..4863852 100644
--- a/polygerrit-ui/app/models/change/change-model.ts
+++ b/polygerrit-ui/app/models/change/change-model.ts
@@ -48,6 +48,7 @@
   createEditUrl,
 } from '../views/change';
 import {NavigationService} from '../../elements/core/gr-navigation/gr-navigation';
+import {getRevertCreatedChangeIds} from '../../utils/message-util';
 
 export enum LoadingStatus {
   NOT_LOADED = 'NOT_LOADED',
@@ -332,6 +333,12 @@
     ([change, account]) => isOwner(change, account)
   );
 
+  public readonly messages$ = select(this.change$, change => change?.messages);
+
+  public readonly revertingChangeIds$ = select(this.messages$, messages =>
+    getRevertCreatedChangeIds(messages ?? [])
+  );
+
   // For usage in `combineLatest` we need `startWith` such that reload$ has an
   // initial value.
   readonly reload$: Observable<unknown> = fromEvent(document, 'reload').pipe(
diff --git a/polygerrit-ui/app/models/change/related-changes-model.ts b/polygerrit-ui/app/models/change/related-changes-model.ts
index 02d9f88..901999c 100644
--- a/polygerrit-ui/app/models/change/related-changes-model.ts
+++ b/polygerrit-ui/app/models/change/related-changes-model.ts
@@ -13,10 +13,11 @@
 import {Model} from '../model';
 import {define} from '../dependency';
 import {ChangeModel} from './change-model';
-import {combineLatest, from, of} from 'rxjs';
-import {switchMap} from 'rxjs/operators';
+import {combineLatest, forkJoin, from, of} from 'rxjs';
+import {map, switchMap} from 'rxjs/operators';
 import {ConfigModel} from '../config/config-model';
 import {ChangeStatus} from '../../api/rest-api';
+import {isDefined} from '../../types/types';
 
 export interface RelatedChangesState {
   /** `undefined` means "not yet loaded". */
@@ -25,6 +26,7 @@
   cherryPicks?: ChangeInfo[];
   conflictingChanges?: ChangeInfo[];
   sameTopicChanges?: ChangeInfo[];
+  revertingChanges: ChangeInfo[];
 }
 
 const initialState: RelatedChangesState = {
@@ -33,6 +35,7 @@
   cherryPicks: undefined,
   conflictingChanges: undefined,
   sameTopicChanges: undefined,
+  revertingChanges: [],
 };
 
 export const relatedChangesModelToken = define<RelatedChangesModel>(
@@ -66,6 +69,32 @@
   );
 
   /**
+   * Emits all changes that have reverted the current change, based on
+   * information from parsed change messages. Abandoned changes are not
+   * included.
+   */
+  public readonly revertingChanges$ = select(
+    this.state$,
+    state => state.revertingChanges
+  );
+
+  /**
+   * Emits one reverting change (if there is any) from revertingChanges$.
+   * It prefers MERGED changes. Otherwise the choice is random.
+   */
+  public readonly revertingChange$ = select(
+    this.revertingChanges$,
+    revertingChanges => {
+      if (revertingChanges.length === 0) return undefined;
+      const submittedRevert = revertingChanges.find(
+        c => c.status === ChangeStatus.MERGED
+      );
+      if (submittedRevert) return submittedRevert;
+      return revertingChanges[0];
+    }
+  );
+
+  /**
    * Determines whether the change has a parent change. If there
    * is a relation chain, and the change id is not the last item of the
    * relation chain, then there is a parent.
@@ -93,6 +122,7 @@
       this.loadCherryPicks(),
       this.loadConflictingChanges(),
       this.loadSameTopicChanges(),
+      this.loadRevertingChanges(),
     ];
   }
 
@@ -197,4 +227,26 @@
         this.updateState({sameTopicChanges});
       });
   }
+
+  private loadRevertingChanges() {
+    return combineLatest([
+      this.changeModel.reload$,
+      this.changeModel.revertingChangeIds$,
+    ])
+      .pipe(
+        switchMap(([_, changeIds]) => {
+          if (!changeIds?.length) return of([]);
+          return forkJoin(
+            changeIds.map(changeId =>
+              from(this.restApiService.getChange(changeId))
+            )
+          );
+        }),
+        map(changes => changes.filter(isDefined)),
+        map(changes => changes.filter(c => c.status !== ChangeStatus.ABANDONED))
+      )
+      .subscribe(revertingChanges => {
+        this.updateState({revertingChanges});
+      });
+  }
 }
diff --git a/polygerrit-ui/app/models/change/related-changes-model_test.ts b/polygerrit-ui/app/models/change/related-changes-model_test.ts
index 31c0026..295f284 100644
--- a/polygerrit-ui/app/models/change/related-changes-model_test.ts
+++ b/polygerrit-ui/app/models/change/related-changes-model_test.ts
@@ -22,8 +22,10 @@
   createRelatedChangesInfo,
   createRelatedChangeAndCommitInfo,
   createChange,
+  createChangeMessage,
 } from '../../test/test-data-generators';
-import {ChangeStatus, TopicName} from '../../api/rest-api';
+import {ChangeStatus, ReviewInputTag, TopicName} from '../../api/rest-api';
+import {MessageTag} from '../../constants/constants';
 
 suite('related-changes-model tests', () => {
   let model: RelatedChangesModel;
@@ -235,4 +237,50 @@
       assert.isTrue(getChangesWithSameTopicStub.calledOnce);
     });
   });
+
+  suite('loadRevertingChanges', async () => {
+    let getChangeStub: SinonStub;
+
+    setup(() => {
+      getChangeStub = stubRestApi('getChange').callsFake(() =>
+        Promise.resolve(createChange())
+      );
+    });
+
+    test('revertingChanges initially empty', async () => {
+      await waitUntilObserved(
+        model.revertingChanges$,
+        revertingChanges => revertingChanges.length === 0
+      );
+      assert.isFalse(getChangeStub.called);
+    });
+
+    test('revertingChanges empty when change does not contain a revert message', async () => {
+      changeModel.updateStateChange(createParsedChange());
+      await waitUntilObserved(
+        model.revertingChanges$,
+        revertingChanges => revertingChanges.length === 0
+      );
+      assert.isFalse(getChangeStub.called);
+    });
+
+    test('revertingChanges emits', async () => {
+      changeModel.updateStateChange({
+        ...createParsedChange(),
+        messages: [
+          {
+            ...createChangeMessage(),
+            message: 'Created a revert of this change as 123',
+            tag: MessageTag.TAG_REVERT as ReviewInputTag,
+          },
+        ],
+      });
+
+      await waitUntilObserved(
+        model.revertingChanges$,
+        revertingChanges => revertingChanges?.length === 1
+      );
+      assert.isTrue(getChangeStub.calledOnce);
+    });
+  });
 });
diff --git a/polygerrit-ui/app/utils/change-util.ts b/polygerrit-ui/app/utils/change-util.ts
index 5dda8d6..4490afa 100644
--- a/polygerrit-ui/app/utils/change-util.ts
+++ b/polygerrit-ui/app/utils/change-util.ts
@@ -20,6 +20,8 @@
 interface ChangeStatusesOptions {
   mergeable: boolean; // This can be wrong! See WARNING above
   submitEnabled: boolean; // This can be wrong! See WARNING above
+  /** Is there a reverting change and if so, what status has it? */
+  revertingChangeStatus?: ChangeStatus;
 }
 
 export const ChangeDiffType = {
@@ -158,6 +160,12 @@
 ): ChangeStates[] {
   const states = [];
   if (change.status === ChangeStatus.MERGED) {
+    if (options?.revertingChangeStatus === ChangeStatus.MERGED) {
+      return [ChangeStates.MERGED, ChangeStates.REVERT_SUBMITTED];
+    }
+    if (options?.revertingChangeStatus !== undefined) {
+      return [ChangeStates.MERGED, ChangeStates.REVERT_CREATED];
+    }
     return [ChangeStates.MERGED];
   }
   if (change.status === ChangeStatus.ABANDONED) {
diff --git a/polygerrit-ui/app/utils/change-util_test.ts b/polygerrit-ui/app/utils/change-util_test.ts
index ccaa1db..f768145 100644
--- a/polygerrit-ui/app/utils/change-util_test.ts
+++ b/polygerrit-ui/app/utils/change-util_test.ts
@@ -129,6 +129,32 @@
     assert.deepEqual(changeStatuses(change), [ChangeStates.MERGED]);
   });
 
+  test('Merged and Reverted status', () => {
+    const change = {
+      ...createChange(),
+      revisions: createRevisions(1),
+      current_revision: 'rev1' as CommitId,
+      status: ChangeStatus.MERGED,
+    };
+    assert.deepEqual(changeStatuses(change), [ChangeStates.MERGED]);
+    assert.deepEqual(
+      changeStatuses(change, {
+        revertingChangeStatus: ChangeStatus.NEW,
+        mergeable: true,
+        submitEnabled: true,
+      }),
+      [ChangeStates.MERGED, ChangeStates.REVERT_CREATED]
+    );
+    assert.deepEqual(
+      changeStatuses(change, {
+        revertingChangeStatus: ChangeStatus.MERGED,
+        mergeable: true,
+        submitEnabled: true,
+      }),
+      [ChangeStates.MERGED, ChangeStates.REVERT_SUBMITTED]
+    );
+  });
+
   test('Abandoned status', () => {
     const change = {
       ...createChange(),
diff --git a/polygerrit-ui/app/utils/message-util_test.ts b/polygerrit-ui/app/utils/message-util_test.ts
new file mode 100644
index 0000000..22a5e4d
--- /dev/null
+++ b/polygerrit-ui/app/utils/message-util_test.ts
@@ -0,0 +1,37 @@
+/**
+ * @license
+ * Copyright 2023 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {getRevertCreatedChangeIds} from './message-util';
+import {assert} from '@open-wc/testing';
+import {MessageTag} from '../constants/constants';
+import {ChangeId, ReviewInputTag} from '../api/rest-api';
+import {createChangeMessage} from '../test/test-data-generators';
+
+suite('message-util tests', () => {
+  test('getRevertCreatedChangeIds', () => {
+    const messages = [
+      {
+        ...createChangeMessage(),
+        message: 'Created a revert of this change as 123',
+        tag: MessageTag.TAG_REVERT as ReviewInputTag,
+      },
+      {
+        ...createChangeMessage(),
+        message: 'Created a revert of this change as xyz',
+        tag: MessageTag.TAG_REVERT as ReviewInputTag,
+      },
+      {
+        ...createChangeMessage(),
+        message: 'Created a revert of this change as abc',
+        tag: undefined,
+      },
+    ];
+
+    assert.deepEqual(getRevertCreatedChangeIds(messages), [
+      '123' as ChangeId,
+      'xyz' as ChangeId,
+    ]);
+  });
+});