Add "flutter downgrade" command (#50506)

diff --git a/packages/flutter_tools/test/commands.shard/hermetic/downgrade_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/downgrade_test.dart
new file mode 100644
index 0000000..db627c2
--- /dev/null
+++ b/packages/flutter_tools/test/commands.shard/hermetic/downgrade_test.dart
@@ -0,0 +1,249 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:file/file.dart';
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/logger.dart';
+import 'package:flutter_tools/src/base/terminal.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/commands/downgrade.dart';
+import 'package:flutter_tools/src/persistent_tool_state.dart';
+import 'package:flutter_tools/src/version.dart';
+import 'package:mockito/mockito.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import '../../src/mocks.dart';
+
+void main() {
+  FileSystem fileSystem;
+  BufferLogger bufferLogger;
+  AnsiTerminal terminal;
+  ProcessManager processManager;
+  MockStdio mockStdio;
+  FlutterVersion flutterVersion;
+
+  setUpAll(() {
+    Cache.disableLocking();
+  });
+
+  tearDownAll(() {
+    Cache.enableLocking();
+  });
+
+  setUp(() {
+    flutterVersion = MockFlutterVersion();
+    mockStdio = MockStdio();
+    processManager = FakeProcessManager.any();
+    terminal = MockTerminal();
+    fileSystem = MemoryFileSystem.test();
+    bufferLogger = BufferLogger(terminal: terminal, outputPreferences: OutputPreferences.test());
+  });
+
+  testUsingContext('Downgrade exits on unknown channel', () async {
+    fileSystem.currentDirectory.childFile('.flutter_tool_state')
+      .writeAsStringSync('{"last-active-master-version":"invalid"}');
+    final DowngradeCommand command = DowngradeCommand(
+      persistentToolState: PersistentToolState.test(directory: fileSystem.currentDirectory, logger: bufferLogger),
+      processManager: processManager,
+      terminal: terminal,
+      stdio: mockStdio,
+      flutterVersion: flutterVersion,
+      logger: bufferLogger,
+    );
+    applyMocksToCommand(command);
+
+    expect(createTestCommandRunner(command).run(const <String>['downgrade']),
+      throwsToolExit(message: 'Flutter is not currently on a known channel.'));
+  });
+
+  testUsingContext('Downgrade exits on no recorded version', () async {
+    when(flutterVersion.channel).thenReturn('dev');
+    fileSystem.currentDirectory.childFile('.flutter_tool_state')
+      .writeAsStringSync('{"last-active-master-version":"abcd"}');
+    final DowngradeCommand command = DowngradeCommand(
+      persistentToolState: PersistentToolState.test(directory: fileSystem.currentDirectory, logger: bufferLogger),
+      processManager: FakeProcessManager.list(<FakeCommand>[
+        const FakeCommand(
+          command: <String>[
+            'git', 'describe', '--tags', 'abcd'
+          ],
+          exitCode: 0,
+          stdout: 'v1.2.3'
+        )
+      ]),
+      terminal: terminal,
+      stdio: mockStdio,
+      flutterVersion: flutterVersion,
+      logger: bufferLogger,
+    );
+    applyMocksToCommand(command);
+
+    expect(createTestCommandRunner(command).run(const <String>['downgrade']),
+      throwsToolExit(message:
+        'There is no previously recorded version for channel "dev".\n'
+        'Channel "master" was previously on: v1.2.3.'
+      ),
+    );
+  });
+
+  testUsingContext('Downgrade exits on unknown recorded version', () async {
+    when(flutterVersion.channel).thenReturn('master');
+    fileSystem.currentDirectory.childFile('.flutter_tool_state')
+      .writeAsStringSync('{"last-active-master-version":"invalid"}');
+    final DowngradeCommand command = DowngradeCommand(
+      persistentToolState: PersistentToolState.test(directory: fileSystem.currentDirectory, logger: bufferLogger),
+      processManager: FakeProcessManager.list(<FakeCommand>[
+        const FakeCommand(
+          command: <String>[
+            'git', 'describe', '--tags', 'invalid'
+          ],
+          exitCode: 1,
+        )
+      ]),
+      terminal: terminal,
+      stdio: mockStdio,
+      flutterVersion: flutterVersion,
+      logger: bufferLogger,
+    );
+    applyMocksToCommand(command);
+
+    expect(createTestCommandRunner(command).run(const <String>['downgrade']),
+      throwsToolExit(message: 'Failed to parse version for downgrade'));
+  });
+
+   testUsingContext('Downgrade prompts for user input when terminal is attached - y', () async {
+    when(flutterVersion.channel).thenReturn('master');
+    when(mockStdio.hasTerminal).thenReturn(true);
+    fileSystem.currentDirectory.childFile('.flutter_tool_state')
+      .writeAsStringSync('{"last-active-master-version":"g6b00b5e88"}');
+    final DowngradeCommand command = DowngradeCommand(
+      persistentToolState: PersistentToolState.test(directory: fileSystem.currentDirectory, logger: bufferLogger),
+      processManager: processManager,
+      terminal: terminal,
+      stdio: mockStdio,
+      flutterVersion: flutterVersion,
+      logger: bufferLogger,
+    );
+    applyMocksToCommand(command);
+
+    when(terminal.promptForCharInput(
+      const <String>['y', 'n'],
+      prompt: anyNamed('prompt'),
+      logger: anyNamed('logger'),
+    )).thenAnswer((Invocation invocation) async => 'y');
+
+    await createTestCommandRunner(command).run(const <String>['downgrade']);
+
+    verify(terminal.promptForCharInput(
+      const <String>['y', 'n'],
+      prompt: anyNamed('prompt'),
+      logger: anyNamed('logger'),
+    )).called(1);
+    expect(bufferLogger.statusText, contains('Success'));
+  });
+
+   testUsingContext('Downgrade prompts for user input when terminal is attached - n', () async {
+    when(flutterVersion.channel).thenReturn('master');
+    when(mockStdio.hasTerminal).thenReturn(true);
+    fileSystem.currentDirectory.childFile('.flutter_tool_state')
+      .writeAsStringSync('{"last-active-master-version":"g6b00b5e88"}');
+    final DowngradeCommand command = DowngradeCommand(
+      persistentToolState: PersistentToolState.test(directory: fileSystem.currentDirectory, logger: bufferLogger),
+      processManager: processManager,
+      terminal: terminal,
+      stdio: mockStdio,
+      flutterVersion: flutterVersion,
+      logger: bufferLogger,
+    );
+    applyMocksToCommand(command);
+
+    when(terminal.promptForCharInput(
+      const <String>['y', 'n'],
+      prompt: anyNamed('prompt'),
+      logger: anyNamed('logger'),
+    )).thenAnswer((Invocation invocation) async => 'n');
+
+    await createTestCommandRunner(command).run(const <String>['downgrade']);
+
+    verify(terminal.promptForCharInput(
+      const <String>['y', 'n'],
+      prompt: anyNamed('prompt'),
+      logger: anyNamed('logger'),
+    )).called(1);
+    expect(bufferLogger.statusText, isNot(contains('Success')));
+  });
+
+  testUsingContext('Downgrade does not prompt when there is no terminal', () async {
+    when(flutterVersion.channel).thenReturn('master');
+    when(mockStdio.hasTerminal).thenReturn(false);
+    fileSystem.currentDirectory.childFile('.flutter_tool_state')
+      .writeAsStringSync('{"last-active-master-version":"g6b00b5e88"}');
+    final DowngradeCommand command = DowngradeCommand(
+      persistentToolState: PersistentToolState.test(
+        directory: fileSystem.currentDirectory,
+        logger: bufferLogger,
+      ),
+      processManager: processManager,
+      terminal: terminal,
+      stdio: mockStdio,
+      flutterVersion: flutterVersion,
+      logger: bufferLogger,
+    );
+    applyMocksToCommand(command);
+
+    await createTestCommandRunner(command).run(const <String>['downgrade']);
+
+    verifyNever(terminal.promptForCharInput(
+      const <String>['y', 'n'],
+      prompt: anyNamed('prompt'),
+      logger: anyNamed('logger'),
+    ));
+    expect(bufferLogger.statusText, contains('Success'));
+  });
+
+  testUsingContext('Downgrade performs correct git commands', () async {
+    when(flutterVersion.channel).thenReturn('master');
+    when(mockStdio.hasTerminal).thenReturn(false);
+    fileSystem.currentDirectory.childFile('.flutter_tool_state')
+      .writeAsStringSync('{"last-active-master-version":"g6b00b5e88"}');
+    final DowngradeCommand command = DowngradeCommand(
+      persistentToolState: PersistentToolState.test(
+        directory: fileSystem.currentDirectory,
+        logger: bufferLogger,
+      ),
+      processManager: FakeProcessManager.list(<FakeCommand>[
+        const FakeCommand(
+          command: <String>[
+            'git', 'describe', '--tags', 'g6b00b5e88'
+          ],
+          stdout: 'v1.2.3',
+        ),
+        const FakeCommand(
+          command: <String>[
+            'git', 'reset', '--hard', 'g6b00b5e88'
+          ],
+        ),
+        const FakeCommand(
+          command: <String>[
+            'git', 'checkout', 'master', '--'
+          ]
+        ),
+      ]),
+      terminal: terminal,
+      stdio: mockStdio,
+      flutterVersion: flutterVersion,
+      logger: bufferLogger,
+    );
+    applyMocksToCommand(command);
+
+    await createTestCommandRunner(command).run(const <String>['downgrade']);
+
+    expect(bufferLogger.statusText, contains('Success'));
+  });
+}
+
+class MockTerminal extends Mock implements AnsiTerminal {}
+class MockStdio extends Mock implements Stdio {}
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/version_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/version_test.dart
index 98922e0..74596c3 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/version_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/version_test.dart
@@ -8,11 +8,13 @@
 
 import 'package:flutter_tools/src/base/common.dart';
 import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/terminal.dart';
 import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/commands/version.dart';
 import 'package:flutter_tools/src/version.dart';
 import 'package:mockito/mockito.dart';
 import 'package:process/process.dart';
+import 'package:flutter_tools/src/globals.dart' as globals;
 
 import '../../src/common.dart';
 import '../../src/context.dart';
@@ -20,10 +22,18 @@
 
 void main() {
   group('version', () {
+    MockStdio mockStdio;
+
     setUpAll(() {
       Cache.disableLocking();
     });
 
+    setUp(() {
+      mockStdio = MockStdio();
+      when(mockStdio.stdinHasTerminal).thenReturn(false);
+      when(mockStdio.hasTerminal).thenReturn(false);
+    });
+
     testUsingContext('version ls', () async {
       final VersionCommand command = VersionCommand();
       await createTestCommandRunner(command).run(<String>[
@@ -33,11 +43,18 @@
       expect(testLogger.statusText, equals('v10.0.0\r\nv20.0.0\n'));
     }, overrides: <Type, Generator>{
       ProcessManager: () => MockProcessManager(),
+      Stdio: () => mockStdio,
     });
 
-    testUsingContext('version switch', () async {
+    testUsingContext('version switch prompt is accepted', () async {
+      when(mockStdio.stdinHasTerminal).thenReturn(true);
       const String version = '10.0.0';
       final VersionCommand command = VersionCommand();
+      when(globals.terminal.promptForCharInput(<String>['y', 'n'],
+        logger: anyNamed('logger'),
+        prompt: 'Are you sure you want to proceed?')
+      ).thenAnswer((Invocation invocation) async => 'y');
+
       await createTestCommandRunner(command).run(<String>[
         'version',
         '--no-pub',
@@ -46,6 +63,29 @@
       expect(testLogger.statusText, contains('Switching Flutter to version $version'));
     }, overrides: <Type, Generator>{
       ProcessManager: () => MockProcessManager(),
+      Stdio: () => mockStdio,
+      AnsiTerminal: () => MockTerminal(),
+    });
+
+    testUsingContext('version switch prompt is declined', () async {
+      when(mockStdio.stdinHasTerminal).thenReturn(true);
+      const String version = '10.0.0';
+      final VersionCommand command = VersionCommand();
+      when(globals.terminal.promptForCharInput(<String>['y', 'n'],
+        logger: anyNamed('logger'),
+        prompt: 'Are you sure you want to proceed?')
+      ).thenAnswer((Invocation invocation) async => 'n');
+
+      await createTestCommandRunner(command).run(<String>[
+        'version',
+        '--no-pub',
+        version,
+      ]);
+      expect(testLogger.statusText, isNot(contains('Switching Flutter to version $version')));
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => MockProcessManager(),
+      Stdio: () => mockStdio,
+      AnsiTerminal: () => MockTerminal(),
     });
 
     testUsingContext('version switch, latest commit query fails', () async {
@@ -59,6 +99,7 @@
       expect(testLogger.errorText, contains('git failed'));
     }, overrides: <Type, Generator>{
       ProcessManager: () => MockProcessManager(latestCommitFails: true),
+      Stdio: () => mockStdio,
     });
 
     testUsingContext('latest commit is parsable when query fails', () {
@@ -69,6 +110,7 @@
       );
     }, overrides: <Type, Generator>{
       ProcessManager: () => MockProcessManager(latestCommitFails: true),
+      Stdio: () => mockStdio,
     });
 
     testUsingContext('switch to not supported version without force', () async {
@@ -82,6 +124,7 @@
       expect(testLogger.errorText, contains('Version command is not supported in'));
     }, overrides: <Type, Generator>{
       ProcessManager: () => MockProcessManager(),
+      Stdio: () => mockStdio,
     });
 
     testUsingContext('switch to not supported version with force', () async {
@@ -96,6 +139,7 @@
       expect(testLogger.statusText, contains('Switching Flutter to version $version with force'));
     }, overrides: <Type, Generator>{
       ProcessManager: () => MockProcessManager(),
+      Stdio: () => mockStdio,
     });
 
     testUsingContext('tool exit on confusing version', () async {
@@ -111,6 +155,7 @@
       );
     }, overrides: <Type, Generator>{
       ProcessManager: () => MockProcessManager(),
+      Stdio: () => mockStdio,
     });
 
     testUsingContext("exit tool if can't get the tags", () async {
@@ -123,10 +168,13 @@
       }
     }, overrides: <Type, Generator>{
       ProcessManager: () => MockProcessManager(failGitTag: true),
+      Stdio: () => mockStdio,
     });
   });
 }
 
+class MockTerminal extends Mock implements AnsiTerminal {}
+class MockStdio extends Mock implements Stdio {}
 class MockProcessManager extends Mock implements ProcessManager {
   MockProcessManager({
     this.failGitTag = false,
diff --git a/packages/flutter_tools/test/commands.shard/permeable/upgrade_test.dart b/packages/flutter_tools/test/commands.shard/permeable/upgrade_test.dart
index 1d71094..f5ac6fb 100644
--- a/packages/flutter_tools/test/commands.shard/permeable/upgrade_test.dart
+++ b/packages/flutter_tools/test/commands.shard/permeable/upgrade_test.dart
@@ -57,10 +57,11 @@
 
     testUsingContext('throws on unknown tag, official branch,  noforce', () async {
       final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
-        false,
-        false,
-        const GitTagVersion.unknown(),
-        flutterVersion,
+        force: false,
+        continueFlow: false,
+        testFlow: false,
+        gitTagVersion: const GitTagVersion.unknown(),
+        flutterVersion: flutterVersion,
       );
       expect(result, throwsToolExit());
     }, overrides: <Type, Generator>{
@@ -69,10 +70,11 @@
 
     testUsingContext('does not throw on unknown tag, official branch, force', () async {
       final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
-        true,
-        false,
-        const GitTagVersion.unknown(),
-        flutterVersion,
+        force: true,
+        continueFlow: false,
+        testFlow: false,
+        gitTagVersion: const GitTagVersion.unknown(),
+        flutterVersion: flutterVersion,
       );
       expect(await result, FlutterCommandResult.success());
     }, overrides: <Type, Generator>{
@@ -83,10 +85,11 @@
     testUsingContext('throws tool exit with uncommitted changes', () async {
       fakeCommandRunner.willHaveUncomittedChanges = true;
       final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
-        false,
-        false,
-        gitTagVersion,
-        flutterVersion,
+        force: false,
+        continueFlow: false,
+        testFlow: false,
+        gitTagVersion: gitTagVersion,
+        flutterVersion: flutterVersion,
       );
       expect(result, throwsToolExit());
     }, overrides: <Type, Generator>{
@@ -97,10 +100,11 @@
       fakeCommandRunner.willHaveUncomittedChanges = true;
 
       final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
-        true,
-        false,
-        gitTagVersion,
-        flutterVersion,
+        force: true,
+        continueFlow: false,
+        testFlow: false,
+        gitTagVersion: gitTagVersion,
+        flutterVersion: flutterVersion,
       );
       expect(await result, FlutterCommandResult.success());
     }, overrides: <Type, Generator>{
@@ -110,10 +114,11 @@
 
     testUsingContext("Doesn't throw on known tag, dev branch, no force", () async {
       final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
-        false,
-        false,
-        gitTagVersion,
-        flutterVersion,
+        force: false,
+        continueFlow: false,
+        testFlow: false,
+        gitTagVersion: gitTagVersion,
+        flutterVersion: flutterVersion,
       );
       expect(await result, FlutterCommandResult.success());
     }, overrides: <Type, Generator>{
@@ -124,10 +129,11 @@
     testUsingContext("Doesn't continue on known tag, dev branch, no force, already up-to-date", () async {
       fakeCommandRunner.alreadyUpToDate = true;
       final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
-        false,
-        false,
-        gitTagVersion,
-        flutterVersion,
+        force: false,
+        continueFlow: false,
+        testFlow: false,
+        gitTagVersion: gitTagVersion,
+        flutterVersion: flutterVersion,
       );
       expect(await result, FlutterCommandResult.success());
       verifyNever(globals.processManager.start(
diff --git a/packages/flutter_tools/test/general.shard/persistent_tool_state_test.dart b/packages/flutter_tools/test/general.shard/persistent_tool_state_test.dart
index 59dead0..1f4bfcf 100644
--- a/packages/flutter_tools/test/general.shard/persistent_tool_state_test.dart
+++ b/packages/flutter_tools/test/general.shard/persistent_tool_state_test.dart
@@ -6,6 +6,7 @@
 import 'package:flutter_tools/src/base/file_system.dart';
 import 'package:flutter_tools/src/base/logger.dart';
 import 'package:flutter_tools/src/persistent_tool_state.dart';
+import 'package:flutter_tools/src/version.dart';
 import 'package:mockito/mockito.dart';
 
 import '../src/common.dart';
@@ -14,8 +15,8 @@
 
 void main() {
   testWithoutContext('state can be set and persists', () {
-    final MemoryFileSystem fs = MemoryFileSystem();
-    final Directory directory = fs.directory('state_dir');
+    final MemoryFileSystem fileSystem = MemoryFileSystem();
+    final Directory directory = fileSystem.directory('state_dir');
     directory.createSync();
     final File stateFile = directory.childFile('.flutter_tool_state');
     final PersistentToolState state1 = PersistentToolState.test(
@@ -35,4 +36,28 @@
     );
     expect(state2.redisplayWelcomeMessage, false);
   });
+
+  testWithoutContext('channel versions can be cached and stored', () {
+    final MemoryFileSystem fileSystem = MemoryFileSystem();
+    final Directory directory = fileSystem.directory('state_dir')..createSync();
+    final PersistentToolState state1 = PersistentToolState.test(
+      directory: directory,
+      logger: MockLogger(),
+    );
+
+    state1.updateLastActiveVersion('abc', Channel.master);
+    state1.updateLastActiveVersion('def', Channel.dev);
+    state1.updateLastActiveVersion('ghi', Channel.beta);
+    state1.updateLastActiveVersion('jkl', Channel.stable);
+
+    final PersistentToolState state2 = PersistentToolState.test(
+      directory: directory,
+      logger: MockLogger(),
+    );
+
+    expect(state2.lastActiveVersion(Channel.master), 'abc');
+    expect(state2.lastActiveVersion(Channel.dev), 'def');
+    expect(state2.lastActiveVersion(Channel.beta), 'ghi');
+    expect(state2.lastActiveVersion(Channel.stable), 'jkl');
+  });
 }
diff --git a/packages/flutter_tools/test/integration.shard/downgrade_upgrade_integration_test.dart b/packages/flutter_tools/test/integration.shard/downgrade_upgrade_integration_test.dart
new file mode 100644
index 0000000..6c1bc2c
--- /dev/null
+++ b/packages/flutter_tools/test/integration.shard/downgrade_upgrade_integration_test.dart
@@ -0,0 +1,122 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/logger.dart';
+import 'package:flutter_tools/src/base/process.dart';
+import 'package:flutter_tools/src/base/terminal.dart';
+import 'package:platform/platform.dart';
+import 'package:process/process.dart';
+
+import '../src/common.dart';
+
+const String _kInitialVersion = 'v1.9.1+hotfix.6';
+const String _kBranch = 'stable';
+const FileSystem fileSystem = LocalFileSystem();
+const ProcessManager processManager = LocalProcessManager();
+final ProcessUtils processUtils = ProcessUtils(processManager: processManager, logger: StdoutLogger(
+  terminal: AnsiTerminal(
+    platform: const LocalPlatform(),
+    stdio: const Stdio(),
+  ),
+  stdio: const Stdio(),
+  outputPreferences: OutputPreferences.test(wrapText: true),
+  timeoutConfiguration: const TimeoutConfiguration(),
+));
+final String flutterBin = fileSystem.path.join(getFlutterRoot(), 'bin', 'flutter');
+
+/// A test for flutter upgrade & downgrade that checks out a parallel flutter repo.
+void main() {
+  Directory parentDirectory;
+
+  setUp(() {
+    parentDirectory = fileSystem.systemTempDirectory
+      .createTempSync('flutter_tools.');
+    parentDirectory.createSync(recursive: true);
+  });
+
+  tearDown(() {
+    try {
+      parentDirectory.deleteSync(recursive: true);
+    } on FileSystemException {
+      print('Failed to delete test directory');
+    }
+  });
+
+  test('Can upgrade and downgrade a Flutter checkout', () async {
+    final Directory testDirectory = parentDirectory.childDirectory('flutter');
+    testDirectory.createSync(recursive: true);
+
+    // Enable longpaths for windows integration test.
+    await processManager.run(<String>[
+      'git', 'config', '--system', 'core.longpaths', 'true',
+    ]);
+
+    // Step 1. Clone the dev branch of flutter into the test directory.
+    await processUtils.stream(<String>[
+      'git',
+      'clone',
+      'https://github.com/flutter/flutter.git',
+    ], workingDirectory: parentDirectory.path, trace: true);
+
+    // Step 2. Switch to the dev branch.
+    await processUtils.stream(<String>[
+      'git',
+      'checkout',
+      '--track',
+      '-b',
+      _kBranch,
+      'origin/$_kBranch',
+    ], workingDirectory: testDirectory.path, trace: true);
+
+    // Step 3. Revert to a prior version.
+    await processUtils.stream(<String>[
+      'git',
+      'reset',
+      '--hard',
+      _kInitialVersion,
+    ], workingDirectory: testDirectory.path, trace: true);
+
+    // Step 4. Upgrade to the newest dev. This should update the persistent
+    // tool state with the sha for v1.14.3
+    await processUtils.stream(<String>[
+      flutterBin,
+      'upgrade',
+      '--working-directory=${testDirectory.path}'
+    ], workingDirectory: testDirectory.path, trace: true);
+
+    // Step 5. Verify that the version is different.
+    final RunResult versionResult = await processUtils.run(<String>[
+      'git',
+      'describe',
+      '--match',
+      'v*.*.*',
+      '--first-parent',
+      '--long',
+      '--tags',
+    ], workingDirectory: testDirectory.path);
+    expect(versionResult.stdout, isNot(contains(_kInitialVersion)));
+
+    // Step 6. Downgrade back to initial version.
+    await processUtils.stream(<String>[
+       flutterBin,
+      'downgrade',
+      '--no-prompt',
+      '--working-directory=${testDirectory.path}'
+    ], workingDirectory: testDirectory.path, trace: true);
+
+    // Step 7. Verify downgraded version matches original version.
+    final RunResult oldVersionResult = await processUtils.run(<String>[
+      'git',
+      'describe',
+      '--match',
+      'v*.*.*',
+      '--first-parent',
+      '--long',
+      '--tags',
+    ], workingDirectory: testDirectory.path);
+    expect(oldVersionResult.stdout, contains(_kInitialVersion));
+  });
+}