sync: Support manifest-driven default smart sync

Add support for enabling smart sync by default via the
sync-smartsync="true" attribute on the manifest <default> element.

When sync-smartsync is set to true in the manifest defaults:
- Plain `repo sync` triggers Smart Sync automatically.
- If that implicit Smart Sync fails (manifest server unreachable, or
  the returned manifest cannot be parsed), repo silently falls back to
  a regular ToT sync. The reason is logged with --verbose.
- `--no-smart-sync` CLI flag overrides the manifest default and
  syncs to ToT.
- Passing explicit `-s` / `--smart-sync` forces Smart Sync as before,
  and its failures are still fatal.
- `-t`, `--smart-tag`, `-m`, `--manifest-name`, and
  `--superproject-revision` disable the default smart sync.
- `-l` / `--local-only` and `--no-manifest-update` skip the manifest
  server and reuse the existing smart_sync_override.xml, if any.

Update docs, completion, regenerate man pages, and add tests for XML
parsing, serialization, CLI option resolution, and the fallback and
reuse paths.

Bug: 545137090
Change-Id: If75569c58dd525d170e9801e70ccbf02206a52f2
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/617641
Tested-by: Rahul Yadav <yadavrah@google.com>
Commit-Queue: Rahul Yadav <yadavrah@google.com>
Reviewed-by: Gavin Mak <gavinmak@google.com>
diff --git a/completion.zsh b/completion.zsh
index ac82ce7..23b7db1 100644
--- a/completion.zsh
+++ b/completion.zsh
@@ -337,6 +337,7 @@
         '--auto-gc[Run auto gc]' \
         '--no-auto-gc[Do not run auto gc]' \
         '(-s --smart-sync)'{-s,--smart-sync}'[Smart sync]' \
+        '--no-smart-sync[Do not smart sync]' \
         '(-t --smart-tag)'{-t,--smart-tag=}'[Smart tag]:tag:' \
         '--no-repo-verify[Do not verify repo]' \
         '--no-verify[Do not verify]' \
diff --git a/docs/manifest-format.md b/docs/manifest-format.md
index e128a65..18420b8 100644
--- a/docs/manifest-format.md
+++ b/docs/manifest-format.md
@@ -46,18 +46,19 @@
   <!ATTLIST remote revision     CDATA #IMPLIED>
 
   <!ELEMENT default EMPTY>
-  <!ATTLIST default remote      IDREF #IMPLIED>
-  <!ATTLIST default revision    CDATA #IMPLIED>
-  <!ATTLIST default dest-branch CDATA #IMPLIED>
-  <!ATTLIST default upstream    CDATA #IMPLIED>
-  <!ATTLIST default sync-j      CDATA #IMPLIED>
-  <!ATTLIST default sync-j-max  CDATA #IMPLIED>
-  <!ATTLIST default sync-c      CDATA #IMPLIED>
-  <!ATTLIST default sync-s      CDATA #IMPLIED>
-  <!ATTLIST default sync-tags   CDATA #IMPLIED>
+  <!ATTLIST default remote         IDREF #IMPLIED>
+  <!ATTLIST default revision       CDATA #IMPLIED>
+  <!ATTLIST default dest-branch    CDATA #IMPLIED>
+  <!ATTLIST default upstream       CDATA #IMPLIED>
+  <!ATTLIST default sync-j         CDATA #IMPLIED>
+  <!ATTLIST default sync-j-max     CDATA #IMPLIED>
+  <!ATTLIST default sync-c         CDATA #IMPLIED>
+  <!ATTLIST default sync-s         CDATA #IMPLIED>
+  <!ATTLIST default sync-tags      CDATA #IMPLIED>
+  <!ATTLIST default sync-smartsync CDATA #IMPLIED>
 
   <!ELEMENT manifest-server EMPTY>
-  <!ATTLIST manifest-server url CDATA #REQUIRED>
+  <!ATTLIST manifest-server url    CDATA #REQUIRED>
   <!ATTLIST manifest-server helper CDATA #IMPLIED>
 
   <!ELEMENT submanifest EMPTY>
@@ -231,6 +232,8 @@
 branch (specified in the `revision` attribute) rather than
 the other ref tags.
 
+Attribute `sync-smartsync`: Set to true to enable smart sync by default.
+
 
 ### Element manifest-server
 
diff --git a/docs/smart-sync.md b/docs/smart-sync.md
index 9f65945..99270a5 100644
--- a/docs/smart-sync.md
+++ b/docs/smart-sync.md
@@ -138,9 +138,36 @@
 The next time `repo sync` is run, this file is automatically replaced or removed
 based on the current set of options.
 
-### --smart-sync
+### sync-smartsync (Manifest Attribute)
 
-Repo will call `GetApprovedManifest(branch[, target])`.
+The manifest can enable Smart Sync by default for all `repo sync` invocations
+by setting `sync-smartsync="true"` on the `<default>` element:
+
+```xml
+  <default sync-smartsync="true" ... />
+```
+
+When enabled in the manifest, `repo sync` will automatically perform a Smart
+Sync unless explicitly overridden on the command line. Since the user did not
+ask for a Smart Sync, any failure to obtain a manifest from the manifest server
+(e.g. the server is unreachable, or it returns a manifest that cannot be parsed)
+is silently ignored and `repo sync` falls back to a regular ToT sync; pass
+`--verbose` to see the reason. This is only a fallback for the manifest-driven
+default: an explicit `-s`/`--smart-sync` or `-t`/`--smart-tag` still fails the
+sync, as before. Options that specify an explicit target or manifest source
+(such as `-t`/`--smart-tag`, `-m`/`--manifest-name` or
+`--superproject-revision`) will also disable the default Smart Sync behavior.
+
+With `-l`/`--local-only` or `--no-manifest-update`, the default Smart Sync does
+not contact the manifest server. Instead, repo keeps using the
+`smart_sync_override.xml` from the last Smart Sync, if there is one. Pass
+`--no-smart-sync` to discard it and use the default manifest instead. These
+options do not change the behavior of an explicit `-s`/`--smart-sync`.
+
+### --smart-sync / -s
+
+Explicitly enables Smart Sync. Repo will call
+`GetApprovedManifest(branch[, target])`.
 
 The `branch` is determined by the current manifest branch as specified by
 `--manifest-branch=BRANCH` when running `repo init`.
@@ -155,6 +182,11 @@
 3.  `${TARGET_PRODUCT}-${TARGET_BUILD_VARIANT}`: If these variables are all
     defined, then they are merged with `-` and used.
 
-### --smart-tag=TAG
+### --no-smart-sync
+
+Explicitly disables Smart Sync, overriding any `sync-smartsync="true"` setting
+declared in the manifest `<default>` element.
+
+### --smart-tag=TAG / -t
 
 Repo will call `GetManifest(TAG)`.
diff --git a/man/repo-manifest.1 b/man/repo-manifest.1
index 903be25..49923b9 100644
--- a/man/repo-manifest.1
+++ b/man/repo-manifest.1
@@ -1,5 +1,5 @@
 .\" DO NOT MODIFY THIS FILE!  It was generated by help2man.
-.TH REPO "1" "June 2026" "repo manifest" "Repo Manual"
+.TH REPO "1" "August 2026" "repo manifest" "Repo Manual"
 .SH NAME
 repo \- repo manifest - manual page for repo manifest
 .SH SYNOPSIS
@@ -121,18 +121,19 @@
 <!ATTLIST remote revision     CDATA #IMPLIED>
 .IP
 <!ELEMENT default EMPTY>
-<!ATTLIST default remote      IDREF #IMPLIED>
-<!ATTLIST default revision    CDATA #IMPLIED>
-<!ATTLIST default dest\-branch CDATA #IMPLIED>
-<!ATTLIST default upstream    CDATA #IMPLIED>
-<!ATTLIST default sync\-j      CDATA #IMPLIED>
-<!ATTLIST default sync\-j\-max  CDATA #IMPLIED>
-<!ATTLIST default sync\-c      CDATA #IMPLIED>
-<!ATTLIST default sync\-s      CDATA #IMPLIED>
-<!ATTLIST default sync\-tags   CDATA #IMPLIED>
+<!ATTLIST default remote         IDREF #IMPLIED>
+<!ATTLIST default revision       CDATA #IMPLIED>
+<!ATTLIST default dest\-branch    CDATA #IMPLIED>
+<!ATTLIST default upstream       CDATA #IMPLIED>
+<!ATTLIST default sync\-j         CDATA #IMPLIED>
+<!ATTLIST default sync\-j\-max     CDATA #IMPLIED>
+<!ATTLIST default sync\-c         CDATA #IMPLIED>
+<!ATTLIST default sync\-s         CDATA #IMPLIED>
+<!ATTLIST default sync\-tags      CDATA #IMPLIED>
+<!ATTLIST default sync\-smartsync CDATA #IMPLIED>
 .IP
 <!ELEMENT manifest\-server EMPTY>
-<!ATTLIST manifest\-server url CDATA #REQUIRED>
+<!ATTLIST manifest\-server url    CDATA #REQUIRED>
 <!ATTLIST manifest\-server helper CDATA #IMPLIED>
 .IP
 <!ELEMENT submanifest EMPTY>
@@ -336,6 +337,8 @@
 Attribute `sync\-tags`: Set to false to only sync the given Git branch (specified
 in the `revision` attribute) rather than the other ref tags.
 .PP
+Attribute `sync\-smartsync`: Set to true to enable smart sync by default.
+.PP
 Element manifest\-server
 .PP
 At most one manifest\-server may be specified. The url attribute is used to
diff --git a/man/repo-sync.1 b/man/repo-sync.1
index 2767ac3..b033a90 100644
--- a/man/repo-sync.1
+++ b/man/repo-sync.1
@@ -125,6 +125,9 @@
 \fB\-s\fR, \fB\-\-smart\-sync\fR
 smart sync using manifest from the latest known good build
 .TP
+\fB\-\-no\-smart\-sync\fR
+disable smart sync and sync to ToT instead
+.TP
 \fB\-t\fR SMART_TAG, \fB\-\-smart\-tag\fR=\fI\,SMART_TAG\/\fR
 smart sync using manifest from a known tag
 .SS Logging options:
diff --git a/manifest_xml.py b/manifest_xml.py
index 0e80435..c3d6fe0 100644
--- a/manifest_xml.py
+++ b/manifest_xml.py
@@ -159,6 +159,7 @@
     sync_c = False
     sync_s = False
     sync_tags = True
+    sync_smartsync = False
 
     def __eq__(self, other):
         if not isinstance(other, _Default):
@@ -483,6 +484,17 @@
         self.Unload()
         self._Load()
 
+    def ClearOverride(self) -> None:
+        """Stop overriding the manifest, reverting to the default one.
+
+        This is the inverse of Override(), and is a no-op if no override is in
+        effect.  It is useful to recover from a failed Override(), which
+        registers the override before parsing it.
+        """
+        self._outer_client.manifestFileOverrides.pop(self.path_prefix, None)
+        self._load_local_manifests = True
+        self.Unload()
+
     def Link(self, name):
         """Update the repo metadata to use a different manifest."""
         self.Override(name)
@@ -644,6 +656,9 @@
         if not d.sync_tags:
             have_default = True
             e.setAttribute("sync-tags", "false")
+        if d.sync_smartsync:
+            have_default = True
+            e.setAttribute("sync-smartsync", "true")
         if have_default:
             root.appendChild(e)
             root.appendChild(doc.createTextNode(""))
@@ -1789,6 +1804,7 @@
         d.sync_c = XmlBool(node, "sync-c", False)
         d.sync_s = XmlBool(node, "sync-s", False)
         d.sync_tags = XmlBool(node, "sync-tags", True)
+        d.sync_smartsync = XmlBool(node, "sync-smartsync", False)
         return d
 
     def _ParseNotice(self, node):
diff --git a/subcmds/sync.py b/subcmds/sync.py
index 0739c82..96fb485 100644
--- a/subcmds/sync.py
+++ b/subcmds/sync.py
@@ -58,6 +58,7 @@
 from command import MirrorSafeCommand
 from command import WORKER_BATCH_SIZE
 from error import GitError
+from error import ManifestParseError
 from error import RepoChangedException
 from error import RepoError
 from error import RepoExitError
@@ -764,10 +765,17 @@
                 "-s",
                 "--smart-sync",
                 action="store_true",
+                default=None,
                 help="smart sync using manifest from the latest known good "
                 "build",
             )
             p.add_option(
+                "--no-smart-sync",
+                dest="smart_sync",
+                action="store_false",
+                help="disable smart sync and sync to ToT instead",
+            )
+            p.add_option(
                 "-t",
                 "--smart-tag",
                 action="store",
@@ -2249,10 +2257,6 @@
         if opt.manifest_name and opt.smart_tag:
             self.OptionParser.error("cannot combine -m and -t")
         if opt.manifest_server_username or opt.manifest_server_password:
-            if not (opt.smart_sync or opt.smart_tag):
-                self.OptionParser.error(
-                    "-u and -p may only be combined with -s or -t"
-                )
             if None in [
                 opt.manifest_server_username,
                 opt.manifest_server_password,
@@ -2413,6 +2417,34 @@
                 "failed to sync manifest project", aggregate_errors=[e]
             )
 
+    def _ResolveSmartSyncOption(
+        self, opt: optparse.Values, manifest: XmlManifest
+    ) -> bool:
+        """Resolve opt.smart_sync from the CLI flags and manifest default.
+
+        Returns:
+            True if smart_sync was enabled implicitly by the manifest default,
+            False otherwise.
+        """
+        implicit = (
+            opt.smart_sync is None
+            and not opt.smart_tag
+            and not opt.manifest_name
+            and not opt.superproject_revision
+            and not opt.local_only
+            and opt.mp_update
+            and getattr(manifest.default, "sync_smartsync", False) is True
+        )
+        if opt.smart_sync is None:
+            opt.smart_sync = implicit
+
+        if (
+            opt.manifest_server_username or opt.manifest_server_password
+        ) and not (opt.smart_sync or opt.smart_tag):
+            raise SmartSyncError("-u and -p may only be combined with -s or -t")
+
+        return implicit
+
     def _ExecuteHelper(self, opt, args, errors):
         manifest = self.outer_manifest
         if not opt.outer_manifest:
@@ -2427,11 +2459,58 @@
         if opt.clone_bundle is None:
             opt.clone_bundle = manifest.CloneBundle
 
+        # An onboarded client run with -l or --no-manifest-update keeps the
+        # smart sync manifest it last synced to instead of contacting the
+        # manifest server. Computed before _ResolveSmartSyncOption, which
+        # makes an unset opt.smart_sync indistinguishable from --no-smart-sync.
+        reuse_smart_sync_override = (
+            (opt.local_only or not opt.mp_update)
+            and opt.smart_sync is None
+            and not opt.smart_tag
+            and not opt.manifest_name
+            and not opt.superproject_revision
+            and getattr(manifest.default, "sync_smartsync", False) is True
+            and os.path.isfile(smart_sync_manifest_path)
+        )
+
+        implicit_smart_sync = self._ResolveSmartSyncOption(opt, manifest)
+
         if opt.smart_sync or opt.smart_tag:
-            manifest_name = self._SmartSyncSetup(
-                opt, smart_sync_manifest_path, manifest
-            )
-        else:
+            try:
+                manifest_name = self._SmartSyncSetup(
+                    opt, smart_sync_manifest_path, manifest
+                )
+            except (SmartSyncError, ManifestParseError) as e:
+                if not implicit_smart_sync:
+                    raise
+                # The user did not ask for a smart sync, so failing to get a
+                # usable manifest from the server should not fail the sync.
+                # Fall back to a regular ToT sync instead.
+                opt.smart_sync = False
+                # Override() records the override before parsing it, so an
+                # unparsable manifest would otherwise leave the client pinned
+                # to a file it cannot load.
+                manifest.ClearOverride()
+                if opt.verbose:
+                    logger.warning(
+                        "warning: smart sync failed; falling back to ToT: %s", e
+                    )
+        elif reuse_smart_sync_override:
+            manifest_name = os.path.basename(smart_sync_manifest_path)
+            try:
+                self._ReloadManifest(manifest_name, manifest)
+            except ManifestParseError as e:
+                reuse_smart_sync_override = False
+                manifest_name = None
+                manifest.ClearOverride()
+                if opt.verbose:
+                    logger.warning(
+                        "warning: failed to load existing smart sync manifest; "
+                        "falling back to ToT: %s",
+                        e,
+                    )
+
+        if not (opt.smart_sync or opt.smart_tag or reuse_smart_sync_override):
             if os.path.isfile(smart_sync_manifest_path):
                 try:
                     platform_utils.remove(smart_sync_manifest_path)
diff --git a/tests/test_manifest_xml.py b/tests/test_manifest_xml.py
index 3f3f50a..a04972a 100644
--- a/tests/test_manifest_xml.py
+++ b/tests/test_manifest_xml.py
@@ -420,6 +420,62 @@
             )
             manifest.ToXml()
 
+    def test_sync_smartsync(self, repo_client: RepoClient) -> None:
+        """Check sync-smartsync handling."""
+        manifest = repo_client.get_xml_manifest(
+            "<manifest><default /></manifest>"
+        )
+        assert manifest.default.sync_smartsync is False
+
+        manifest = repo_client.get_xml_manifest(
+            '<manifest><default sync-smartsync="true" /></manifest>'
+        )
+        assert manifest.default.sync_smartsync is True
+        assert (
+            manifest.ToXml().toxml() == '<?xml version="1.0" ?>'
+            '<manifest><default sync-smartsync="true"/></manifest>'
+        )
+
+        manifest = repo_client.get_xml_manifest(
+            '<manifest><default sync-smartsync="false" /></manifest>'
+        )
+        assert manifest.default.sync_smartsync is False
+        assert manifest.ToXml().toxml() == '<?xml version="1.0" ?><manifest/>'
+
+    def test_clear_override(self, repo_client: RepoClient) -> None:
+        """Check ClearOverride reverts to the default manifest."""
+        manifest_xml_fmt = (
+            '<manifest><remote name="r" fetch="." />'
+            '<default remote="r" revision="main" />'
+            '<project name="%s" /></manifest>'
+        )
+        manifest = repo_client.get_xml_manifest(manifest_xml_fmt % "default")
+        (repo_client.manifest_dir / "good.xml").write_text(
+            manifest_xml_fmt % "override", encoding="utf-8"
+        )
+        (repo_client.manifest_dir / "bad.xml").write_text(
+            "<manifest>", encoding="utf-8"
+        )
+
+        # No override in effect: a no-op.
+        manifest.ClearOverride()
+        assert list(manifest.paths) == ["default"]
+
+        manifest.Override("good.xml")
+        assert list(manifest.paths) == ["override"]
+        manifest.ClearOverride()
+        assert not manifest.manifestFileOverrides
+        assert list(manifest.paths) == ["default"]
+
+        # Override() registers the override before parsing it, so a failed
+        # Override() leaves the client pinned until ClearOverride().
+        with pytest.raises(error.ManifestParseError):
+            manifest.Override("bad.xml")
+        with pytest.raises(error.ManifestParseError):
+            manifest.Load()
+        manifest.ClearOverride()
+        assert list(manifest.paths) == ["default"]
+
 
 class TestIncludeElement:
     """Tests for <include>."""
diff --git a/tests/test_subcmds_sync.py b/tests/test_subcmds_sync.py
index efd9ffe..9d079bb 100644
--- a/tests/test_subcmds_sync.py
+++ b/tests/test_subcmds_sync.py
@@ -21,7 +21,7 @@
 import shutil
 import tempfile
 import time
-from typing import Dict, List, Optional, Tuple
+from typing import Dict, List, Optional, Tuple, Type
 import unittest
 from unittest import mock
 
@@ -29,6 +29,7 @@
 
 import command
 from error import GitError
+from error import ManifestParseError
 from error import RepoExitError
 import manifest_xml
 from project import SyncNetworkHalfResult
@@ -390,6 +391,90 @@
             assert opts.jobs_checkout == jobs_check
 
 
+@pytest.mark.parametrize(
+    "argv, sync_smartsync_manifest, expected_smart_sync, expected_implicit",
+    [
+        ([], False, False, False),
+        ([], None, False, False),
+        ([], True, True, True),
+        (["-s"], False, True, False),
+        (["--smart-sync"], False, True, False),
+        (["--smart-sync"], True, True, False),
+        (["--no-smart-sync"], True, False, False),
+        (["--no-smart-sync"], False, False, False),
+        (["-t", "tag123"], True, False, False),
+        (["--smart-tag=tag123"], True, False, False),
+        (["-m", "other.xml"], True, False, False),
+        (["--manifest-name=other.xml"], True, False, False),
+        (["-l"], True, False, False),
+        (["--local-only"], True, False, False),
+        (["--nmu"], True, False, False),
+        (["--no-manifest-update"], True, False, False),
+        (["--superproject-revision=abc"], True, False, False),
+        (["-s", "-l"], True, True, False),
+        (["-s", "--nmu"], True, True, False),
+        (["-s", "--superproject-revision=abc"], True, True, False),
+    ],
+)
+def test_cli_smart_sync(
+    argv: List[str],
+    sync_smartsync_manifest: Optional[bool],
+    expected_smart_sync: bool,
+    expected_implicit: bool,
+) -> None:
+    """Tests --smart-sync and --no-smart-sync option behavior with manifest
+    default.
+    """
+    manifest = mock.MagicMock()
+    manifest.default.sync_smartsync = sync_smartsync_manifest
+
+    cmd = sync.Sync(manifest=manifest)
+    opts, args = cmd.OptionParser.parse_args(argv)
+    cmd.ValidateOptions(opts, args)
+    implicit = cmd._ResolveSmartSyncOption(opts, manifest)
+    assert opts.smart_sync == expected_smart_sync
+    assert implicit == expected_implicit
+
+
+@pytest.mark.parametrize(
+    "argv, sync_smartsync_manifest, expected_exception",
+    [
+        (["-u", "user", "-p", "pass"], False, sync.SmartSyncError),
+        (["-u", "user", "-p", "pass"], True, None),
+        (["-s", "-u", "user", "-p", "pass"], False, None),
+        (["-t", "tag", "-u", "user", "-p", "pass"], False, None),
+        (
+            ["--no-smart-sync", "-u", "user", "-p", "pass"],
+            True,
+            sync.SmartSyncError,
+        ),
+        (["-l", "-u", "user", "-p", "pass"], True, sync.SmartSyncError),
+        (["--nmu", "-u", "user", "-p", "pass"], True, sync.SmartSyncError),
+        (["-s", "-l", "-u", "user", "-p", "pass"], True, None),
+        (["-u", "user"], False, SystemExit),
+        (["-p", "pass"], False, SystemExit),
+    ],
+)
+def test_cli_manifest_server_credentials(
+    argv: List[str],
+    sync_smartsync_manifest: bool,
+    expected_exception: Optional[Type[BaseException]],
+) -> None:
+    """Tests -u and -p validation rules."""
+    manifest = mock.MagicMock()
+    manifest.default.sync_smartsync = sync_smartsync_manifest
+
+    cmd = sync.Sync(manifest=manifest)
+    opts, args = cmd.OptionParser.parse_args(argv)
+    if expected_exception:
+        with pytest.raises(expected_exception):
+            cmd.ValidateOptions(opts, args)
+            cmd._ResolveSmartSyncOption(opts, manifest)
+    else:
+        cmd.ValidateOptions(opts, args)
+        cmd._ResolveSmartSyncOption(opts, manifest)
+
+
 class LocalSyncState(unittest.TestCase):
     """Tests for LocalSyncState."""
 
@@ -1484,6 +1569,193 @@
         phased, interleaved = self._ExecuteUntilSync([])
         self.assertTrue(phased.called or interleaved.called)
 
+    def test_implicit_smart_sync_fallback(self) -> None:
+        """Ensure implicit smart sync silently falls back to ToT."""
+        self.manifest.default.sync_smartsync = True
+        self.opt.smart_sync = None
+        mock.patch.object(self.cmd, "_UpdateAllManifestProjects").start()
+        self.opt.verbose = False
+        with mock.patch.object(
+            self.cmd,
+            "_SmartSyncSetup",
+            side_effect=sync.SmartSyncError("unreachable"),
+        ) as mock_setup:
+            with mock.patch.object(self.cmd, "_UpdateRepoProject"):
+                with mock.patch.object(
+                    self.cmd, "_ValidateOptionsWithManifest"
+                ):
+                    with mock.patch.object(self.cmd, "_SyncInterleaved"):
+                        with mock.patch.object(self.cmd, "_RunPostSyncHook"):
+                            with mock.patch.object(
+                                sync.logger, "warning"
+                            ) as mock_warn:
+                                self.cmd.Execute(self.opt, [])
+        mock_setup.assert_called_once()
+        self.assertFalse(self.opt.smart_sync)
+        self.assertFalse(mock_warn.called)
+
+    def test_implicit_smart_sync_fallback_verbose(self) -> None:
+        """Ensure the fallback reason is logged when --verbose is given."""
+        self.manifest.default.sync_smartsync = True
+        self.opt.smart_sync = None
+        mock.patch.object(self.cmd, "_UpdateAllManifestProjects").start()
+        self.opt.verbose = True
+        with mock.patch.object(
+            self.cmd,
+            "_SmartSyncSetup",
+            side_effect=sync.SmartSyncError("unreachable"),
+        ):
+            with mock.patch.object(self.cmd, "_UpdateRepoProject"):
+                with mock.patch.object(
+                    self.cmd, "_ValidateOptionsWithManifest"
+                ):
+                    with mock.patch.object(self.cmd, "_SyncInterleaved"):
+                        with mock.patch.object(self.cmd, "_RunPostSyncHook"):
+                            with mock.patch.object(
+                                sync.logger, "warning"
+                            ) as mock_warn:
+                                self.cmd.Execute(self.opt, [])
+        self.assertFalse(self.opt.smart_sync)
+        self.assertTrue(mock_warn.called)
+
+    def test_explicit_smart_sync_error(self) -> None:
+        """Ensure explicit smart sync raises SmartSyncError on failure."""
+        self.manifest.default.sync_smartsync = True
+        self.opt.smart_sync = True
+        self.opt.mp_update = False
+        with mock.patch.object(
+            self.cmd,
+            "_SmartSyncSetup",
+            side_effect=sync.SmartSyncError("unreachable"),
+        ):
+            with self.assertRaises(sync.SmartSyncError):
+                self.cmd.Execute(self.opt, [])
+
+    def test_implicit_smart_sync_unparsable_manifest(self) -> None:
+        """Ensure an unparsable smart sync manifest falls back to ToT."""
+        self.manifest.default.sync_smartsync = True
+        self.opt.smart_sync = None
+        mock.patch.object(self.cmd, "_UpdateAllManifestProjects").start()
+        self.opt.verbose = False
+        with mock.patch.object(
+            self.cmd,
+            "_SmartSyncSetup",
+            side_effect=ManifestParseError("mismatched tag"),
+        ):
+            with mock.patch.object(self.cmd, "_UpdateRepoProject"):
+                with mock.patch.object(
+                    self.cmd, "_ValidateOptionsWithManifest"
+                ):
+                    with mock.patch.object(self.cmd, "_SyncInterleaved"):
+                        with mock.patch.object(self.cmd, "_RunPostSyncHook"):
+                            self.cmd.Execute(self.opt, [])
+        self.assertFalse(self.opt.smart_sync)
+        # A half-applied override would pin the client to a broken manifest.
+        self.manifest.ClearOverride.assert_called_once_with()
+
+    def test_explicit_smart_sync_unparsable_manifest(self) -> None:
+        """Ensure explicit smart sync still reports the parse error."""
+        self.manifest.default.sync_smartsync = True
+        self.opt.smart_sync = True
+        self.opt.mp_update = False
+        with mock.patch.object(
+            self.cmd,
+            "_SmartSyncSetup",
+            side_effect=ManifestParseError("mismatched tag"),
+        ):
+            with self.assertRaises(ManifestParseError):
+                self.cmd.Execute(self.opt, [])
+
+    def _ExecuteWithSmartSyncOverride(
+        self, reload_error: Optional[Exception] = None
+    ) -> Tuple[str, mock.MagicMock, mock.MagicMock, mock.MagicMock]:
+        """Run Execute with an existing smart sync override on disk.
+
+        Returns:
+            The override path, and the _SmartSyncSetup, _ReloadManifest and
+            _UpdateAllManifestProjects mocks.
+        """
+        worktree = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, worktree)
+        self.manifest.manifestProject.worktree = worktree
+        # -l reloads the superproject manifest; keep that out of the way.
+        self.manifest.superproject = None
+        self.opt.verbose = False
+        override_path = os.path.join(worktree, "smart_sync_override.xml")
+        with open(override_path, "w") as f:
+            f.write("<manifest />")
+
+        with contextlib.ExitStack() as stack:
+            for name in (
+                "_UpdateRepoProject",
+                "_ValidateOptionsWithManifest",
+                "_SyncInterleaved",
+                "_RunPostSyncHook",
+            ):
+                stack.enter_context(mock.patch.object(self.cmd, name))
+            setup = stack.enter_context(
+                mock.patch.object(self.cmd, "_SmartSyncSetup")
+            )
+            reload = stack.enter_context(
+                mock.patch.object(
+                    self.cmd, "_ReloadManifest", side_effect=reload_error
+                )
+            )
+            update_mps = stack.enter_context(
+                mock.patch.object(self.cmd, "_UpdateAllManifestProjects")
+            )
+            self.cmd.Execute(self.opt, [])
+        return override_path, setup, reload, update_mps
+
+    def test_smart_sync_override_reused_offline(self) -> None:
+        """Ensure -l/--nmu reuse an onboarded client's smart sync manifest."""
+        for local_only, mp_update in ((True, True), (False, False)):
+            with self.subTest(local_only=local_only, mp_update=mp_update):
+                self.manifest.default.sync_smartsync = True
+                self.opt.smart_sync = None
+                self.opt.local_only = local_only
+                self.opt.mp_update = mp_update
+                path, setup, reload, update_mps = (
+                    self._ExecuteWithSmartSyncOverride()
+                )
+                setup.assert_not_called()
+                reload.assert_called_once_with(
+                    "smart_sync_override.xml", self.manifest
+                )
+                self.assertTrue(os.path.isfile(path))
+                if mp_update:
+                    self.assertEqual(
+                        update_mps.call_args[0][2], "smart_sync_override.xml"
+                    )
+
+    def test_smart_sync_override_removed_offline(self) -> None:
+        """Ensure -l still drops the override when reuse does not apply."""
+        cases = (
+            ("--no-smart-sync", False, True),
+            ("manifest without sync-smartsync", None, False),
+        )
+        for name, smart_sync, sync_smartsync in cases:
+            with self.subTest(name):
+                self.manifest.default.sync_smartsync = sync_smartsync
+                self.opt.smart_sync = smart_sync
+                self.opt.local_only = True
+                path, setup, reload, _ = self._ExecuteWithSmartSyncOverride()
+                setup.assert_not_called()
+                reload.assert_not_called()
+                self.assertFalse(os.path.isfile(path))
+
+    def test_smart_sync_override_unparsable_offline(self) -> None:
+        """Ensure an unloadable reused override falls back to ToT."""
+        self.manifest.default.sync_smartsync = True
+        self.opt.smart_sync = None
+        self.opt.local_only = True
+        path, _, _, update_mps = self._ExecuteWithSmartSyncOverride(
+            reload_error=ManifestParseError("mismatched tag")
+        )
+        self.manifest.ClearOverride.assert_called_once_with()
+        self.assertFalse(os.path.isfile(path))
+        self.assertIsNone(update_mps.call_args[0][2])
+
 
 class SyncUpdateRepoProject(unittest.TestCase):
     """Tests for Sync._UpdateRepoProject."""
@@ -2501,6 +2773,35 @@
 
         self.assertEqual(manifest_name, "manifest.xml")
 
+    @mock.patch("xmlrpc.client.Server")
+    def test_smart_sync_setup_unparsable_manifest(
+        self, mock_server_class: mock.MagicMock
+    ) -> None:
+        """Test _SmartSyncSetup when the server returns a bad manifest.
+
+        The parse error is left alone so that an explicit smart sync keeps
+        reporting it as-is; recovering from it is up to the caller.
+        """
+        self.manifest.manifest_server = (
+            "http://android-smartsync.corp.google.com/manifestserver"
+        )
+        self.manifest.manifest_server_helper = None
+
+        mock_server = mock.MagicMock()
+        mock_server.GetApprovedManifest.return_value = [True, "<manifest>"]
+        mock_server_class.return_value = mock_server
+
+        self.cmd._GetBranch = mock.MagicMock(return_value="main")
+        self.cmd._ReloadManifest = mock.MagicMock(
+            side_effect=ManifestParseError("mismatched tag")
+        )
+
+        with mock.patch("builtins.open", mock.mock_open()):
+            with self.assertRaises(ManifestParseError):
+                self.cmd._SmartSyncSetup(
+                    self.opt, self.smart_sync_manifest_path, self.manifest
+                )
+
     @mock.patch("shutil.which")
     @mock.patch("subprocess.Popen")
     def test_smart_sync_setup_helper_error(self, mock_popen, mock_which):