tests: Add tests for repo status output

Build out status subcommand unit coverage using a minimal fake repo
checkout wired through XmlManifest.

The new tests verify:
- clean status output prints the expected project header
- modified tracked files appear with the expected status marker
- `-o` output includes the orphan section and orphan entries
- branch names shown in status reflect a started non-default branch

Change-Id: Ia7c22593d0bbdc4aed81faeb168b846f3e4016ab
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/558501
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Nasser Grainawi <nasser.grainawi@oss.qualcomm.com>
Commit-Queue: Nasser Grainawi <nasser.grainawi@oss.qualcomm.com>
Reviewed-by: Gavin Mak <gavinmak@google.com>
diff --git a/tests/test_subcmds_status.py b/tests/test_subcmds_status.py
new file mode 100644
index 0000000..6007878
--- /dev/null
+++ b/tests/test_subcmds_status.py
@@ -0,0 +1,220 @@
+# Copyright (C) 2026 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Unittests for the status subcmd."""
+
+import contextlib
+import io
+import os
+from pathlib import Path
+import subprocess
+from typing import List, Tuple
+from unittest import mock
+
+import pytest
+import utils_for_test
+
+import manifest_xml
+import subcmds
+
+
+@pytest.fixture
+def repo_client_checkout(
+    tmp_path: Path,
+) -> Tuple[Path, manifest_xml.XmlManifest]:
+    """Create a basic repo client checkout for status tests."""
+    # Create in a subdir to avoid noise (like the repo_trace file).
+    topdir = tmp_path / "client_checkout"
+    repodir = topdir / ".repo"
+    manifest_dir = repodir / "manifests"
+    manifest_file = repodir / manifest_xml.MANIFEST_FILE_NAME
+
+    repodir.mkdir(parents=True)
+    manifest_dir.mkdir()
+
+    gitdir = repodir / "manifests.git"
+    gitdir.mkdir()
+    (gitdir / "config").write_text(
+        """[remote "origin"]
+                url = https://localhost:0/manifest
+                verbose = false
+            """
+    )
+
+    _init_temp_git_tree(manifest_dir)
+
+    manifest_file.write_text(
+        """
+            <manifest>
+                <remote name="origin" fetch="http://localhost" />
+                <default remote="origin" revision="refs/heads/main" />
+                <project name="proj" path="src/proj" />
+            </manifest>
+        """,
+        encoding="utf-8",
+    )
+
+    (repodir / "projects" / "src" / "proj.git").mkdir(parents=True)
+    (repodir / "project-objects" / "proj.git").mkdir(parents=True)
+
+    worktree = topdir / "src" / "proj"
+    worktree.parent.mkdir(parents=True, exist_ok=True)
+    _init_temp_git_tree(worktree)
+
+    manifest = manifest_xml.XmlManifest(str(repodir), str(manifest_file))
+    return topdir, manifest
+
+
+def _init_temp_git_tree(git_dir: Path) -> None:
+    """Create a new git checkout with an initial commit for testing."""
+    utils_for_test.init_git_tree(git_dir)
+    (git_dir / "README").write_text("init")
+    subprocess.check_call(["git", "add", "README"], cwd=git_dir)
+    subprocess.check_call(["git", "commit", "-q", "-m", "init"], cwd=git_dir)
+
+
+def _run_status(manifest: manifest_xml.XmlManifest, argv: List[str]) -> None:
+    """Run the status subcommand with parsed options against a test manifest."""
+    cmd = subcmds.status.Status()
+    cmd.manifest = manifest
+    cmd.client = mock.MagicMock(globalConfig=manifest.globalConfig)
+
+    opts, args = cmd.OptionParser.parse_args(argv + ["--jobs=1"])
+    cmd.CommonValidateOptions(opts, args)
+
+    cmd.Execute(opts, args)
+
+
+def _status_lines(output: str) -> List[str]:
+    """Normalize path separators and split command output into lines."""
+    return output.replace(os.sep, "/").splitlines()
+
+
+def _assert_project_header(line: str, project_path: str, branch: str) -> None:
+    """Assert a status project header line for a project and branch."""
+    expected = f"project {(project_path + '/ '):<40}branch {branch}"
+    assert line == expected
+
+
+def _assert_orphan_block(lines: List[str], expected: List[str]) -> None:
+    """Assert orphan block header and entries, independent of entry ordering."""
+    assert lines
+    assert lines[0] == ("Objects not within a project (orphans)")
+    orphan_lines = lines[1:]
+    assert len(orphan_lines) == len(expected)
+    assert sorted(orphan_lines) == sorted(expected)
+
+
+def test_orphans_basic(
+    repo_client_checkout: Tuple[Path, manifest_xml.XmlManifest],
+) -> None:
+    """Verify -o output includes project header and orphan block."""
+    topdir, manifest = repo_client_checkout
+    project_path = next(iter(manifest.paths.keys()))
+
+    (topdir / "src" / "orphan_dir").mkdir(parents=True)
+    (topdir / "orphan.txt").write_text("data")
+
+    with contextlib.redirect_stdout(io.StringIO()) as stdout:
+        _run_status(manifest, ["-o"])
+
+    lines = _status_lines(stdout.getvalue())
+    _assert_project_header(lines[0], project_path, "main")
+    _assert_orphan_block(
+        lines[1:],
+        [
+            " --\torphan.txt",
+            " --\tsrc/orphan_dir/",
+        ],
+    )
+
+
+def test_empty_status_without_orphans(
+    repo_client_checkout: Tuple[Path, manifest_xml.XmlManifest],
+) -> None:
+    """Verify clean status without -o prints only the project header line."""
+    _, manifest = repo_client_checkout
+    project_path = next(iter(manifest.paths.keys()))
+
+    with contextlib.redirect_stdout(io.StringIO()) as stdout:
+        _run_status(manifest, [])
+
+    lines = _status_lines(stdout.getvalue())
+    assert len(lines) == 1
+    _assert_project_header(lines[0], project_path, "main")
+
+
+def test_status_without_orphans(
+    repo_client_checkout: Tuple[Path, manifest_xml.XmlManifest],
+) -> None:
+    """Verify modified tracked file appears in status output without -o."""
+    topdir, manifest = repo_client_checkout
+    project_path = next(iter(manifest.paths.keys()))
+
+    (topdir / project_path / "README").write_text("updated")
+
+    with contextlib.redirect_stdout(io.StringIO()) as stdout:
+        _run_status(manifest, [])
+
+    lines = _status_lines(stdout.getvalue())
+    assert len(lines) == 2
+    _assert_project_header(lines[0], project_path, "main")
+    assert lines[1] == " -m\tREADME"
+
+
+def test_status_with_orphans_and_modified_file(
+    repo_client_checkout: Tuple[Path, manifest_xml.XmlManifest],
+) -> None:
+    """Verify modified-file status plus orphan block."""
+    topdir, manifest = repo_client_checkout
+    project_path = next(iter(manifest.paths.keys()))
+
+    (topdir / project_path / "README").write_text("updated")
+    (topdir / "src" / "orphan_dir").mkdir(parents=True)
+    (topdir / "orphan.txt").write_text("data")
+
+    with contextlib.redirect_stdout(io.StringIO()) as stdout:
+        _run_status(manifest, ["-o"])
+
+    lines = _status_lines(stdout.getvalue())
+    _assert_project_header(lines[0], project_path, "main")
+    assert lines[1] == " -m\tREADME"
+    _assert_orphan_block(
+        lines[2:],
+        [
+            " --\torphan.txt",
+            " --\tsrc/orphan_dir/",
+        ],
+    )
+
+
+def test_empty_status_after_start_shows_started_branch(
+    repo_client_checkout: Tuple[Path, manifest_xml.XmlManifest],
+) -> None:
+    """Verify status shows the started branch name when the tree is clean."""
+    topdir, manifest = repo_client_checkout
+
+    project_path = next(iter(manifest.paths.keys()))
+    project_worktree = topdir / project_path
+    started_branch = "topic/test-status-branch"
+    subprocess.check_call(
+        ["git", "checkout", "-q", "-b", started_branch], cwd=project_worktree
+    )
+
+    with contextlib.redirect_stdout(io.StringIO()) as stdout:
+        _run_status(manifest, [])
+
+    lines = _status_lines(stdout.getvalue())
+    assert len(lines) == 1
+    _assert_project_header(lines[0], project_path, started_branch)