Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ installable release; see the roadmap in [README.md](README.md).
### Fixed

- **`project-warm`: sentinel debounce keyed off git-common-dir, not worktree path** ([#161](https://github.com/robotrocketscience/aelfrice/issues/161)). Previously `_project_id` was derived from `git rev-parse --show-toplevel`, giving each worktree of the same repo a distinct sentinel under `~/.aelfrice/projects/<id>/.last_warm`. Two worktrees of one repo share a single DB (via `git-common-dir`), so they should share one sentinel. `resolve_project_root` now calls `git rev-parse --path-format=absolute --show-toplevel --git-common-dir` in a single subprocess and keys `ProjectRef.id` off the git-common-dir while keeping `ProjectRef.root` as the worktree working directory (for `os.chdir` in `_warm_store`). New test `test_resolve_project_root_worktrees_share_id` verifies that two worktrees of one repo produce identical `ProjectRef.id` values.
- **`aelf --advanced` / `aelf --help --advanced`** ([#159](https://github.com/robotrocketscience/aelfrice/issues/159)). The README promised `aelf --help --advanced` would reveal hidden subcommands, but the flag was never wired. `main()` now pre-scans `argv` for `--advanced` before building the parser; when present, it builds a second parser with `show_advanced=True` (plain `HelpFormatter` instead of `_SuppressSubparsersFormatter`) and prints the full subcommand list — including `project-warm`, `session-delta`, `rebuild`, `bench`, `migrate`, and all other `help=argparse.SUPPRESS` verbs — then exits 0. `aelf --advanced` alone and `aelf --help --advanced` behave identically. The existing `_SuppressSubparsersFormatter` hiding mechanism is unchanged. 16 new deterministic tests in `tests/test_cli_advanced_help.py`.

### Added

Expand Down
43 changes: 40 additions & 3 deletions src/aelfrice/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1893,7 +1893,22 @@ def _format_action(self, action: argparse.Action) -> str:
return super()._format_action(action)


def build_parser() -> argparse.ArgumentParser:
def build_parser(*, show_advanced: bool = False) -> argparse.ArgumentParser:
"""Build the top-level argument parser.

Parameters
----------
show_advanced:
When *True* the parser uses the plain :class:`argparse.HelpFormatter`
so every subcommand (including those registered with
``help=argparse.SUPPRESS``) appears in ``--help`` output. This is
the behaviour triggered by ``aelf --advanced [--help]``.
When *False* (the default) :class:`_SuppressSubparsersFormatter` hides
the suppressed subcommands from ``--help``.
"""
formatter = (
argparse.HelpFormatter if show_advanced else _SuppressSubparsersFormatter
)
parser = argparse.ArgumentParser(
prog="aelf",
description=(
Expand All @@ -1903,7 +1918,7 @@ def build_parser() -> argparse.ArgumentParser:
"points) are hidden from --help. See docs/COMMANDS.md for the "
"complete reference."
),
formatter_class=_SuppressSubparsersFormatter,
formatter_class=formatter,
)
parser.add_argument(
"--version",
Expand Down Expand Up @@ -2456,11 +2471,33 @@ def main(argv: Sequence[str] | None = None, out: object = None) -> int:
Both are skipped if AELF_NO_UPDATE_CHECK is set, and the banner
is skipped for commands that already handle update messaging
themselves (upgrade / uninstall / statusline).

``--advanced`` flag
-------------------
``aelf --advanced`` (or ``aelf --help --advanced``) prints the full help
output including subcommands that are hidden from the default ``--help``
view (those registered with ``help=argparse.SUPPRESS``). ``--advanced``
is a *help-modifier* flag: it is consumed before argparse sees the rest of
the argv, and always results in help being printed then a clean exit (0).
"""
if out is None:
out = sys.stdout

# Pre-scan argv for --advanced *before* building the parser. We consume
# the flag here rather than registering it with argparse so that it can
# coexist naturally with --help / -h without argparse complaining about
# conflicting actions.
effective_argv: list[str] = list(argv) if argv is not None else sys.argv[1:]
show_advanced = "--advanced" in effective_argv
if show_advanced:
adv_parser = build_parser(show_advanced=True)
# Print full help to *out* (not stdout) so tests can capture it.
adv_parser.print_help(file=out) # type: ignore[arg-type]
print("", file=out) # type: ignore[arg-type]
return 0

parser = build_parser()
args = parser.parse_args(argv)
args = parser.parse_args(effective_argv)
cmd = getattr(args, "cmd", None)
if not _update_check_disabled() and cmd not in _UPDATE_CHECK_SKIP_CMDS:
# Fire-and-forget: cache TTL gates duplicate work, never blocks.
Expand Down
158 changes: 158 additions & 0 deletions tests/test_cli_advanced_help.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
"""tests for `aelf --help --advanced` / `aelf --advanced`.

Issue #159: README claims `aelf --help --advanced` lists hidden subcommands,
but the flag was not wired. These tests assert the wired behaviour.

Design constraints (per project policy):
- deterministic (no I/O, no subprocess)
- ≤1 s each
- each asserts exactly one property
"""
from __future__ import annotations

import io
from pathlib import Path

import pytest

from aelfrice.cli import build_parser, main


@pytest.fixture(autouse=True)
def isolated_db(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
"""Each test gets its own throwaway DB so there are no side-effects."""
p = tmp_path / "aelf.db"
monkeypatch.setenv("AELFRICE_DB", str(p))
return p


# Subcommands that must be HIDDEN from the default --help surface.
_ADVANCED_SUBCOMMANDS = [
"project-warm",
"session-delta",
"rebuild",
"demote",
"validate",
"resolve",
"feedback",
"migrate",
"bench",
"ingest-transcript",
"uninstall",
"unsetup",
"upgrade",
"statusline",
]


def _advanced_help(argv: list[str]) -> tuple[int, str]:
buf = io.StringIO()
code = main(argv=argv, out=buf)
return code, buf.getvalue()


def _default_help() -> str:
"""Return the parser's default --help text (without invoking sys.exit)."""
parser = build_parser()
buf = io.StringIO()
parser.print_help(file=buf)
return buf.getvalue()


# ---------------------------------------------------------------------------
# 1. Default --help hides advanced subcommands
# ---------------------------------------------------------------------------


def test_help_default_hides_project_warm() -> None:
assert "project-warm" not in _default_help()


def test_help_default_hides_session_delta() -> None:
assert "session-delta" not in _default_help()


def test_help_default_hides_rebuild() -> None:
assert "rebuild" not in _default_help()


def test_help_default_hides_bench() -> None:
assert "bench" not in _default_help()


# ---------------------------------------------------------------------------
# 2. `aelf --advanced` shows all hidden subcommands and exits 0
# ---------------------------------------------------------------------------


def test_advanced_alone_exits_zero() -> None:
code, _ = _advanced_help(["--advanced"])
assert code == 0


def test_advanced_alone_shows_project_warm() -> None:
_, output = _advanced_help(["--advanced"])
assert "project-warm" in output


def test_advanced_alone_shows_session_delta() -> None:
_, output = _advanced_help(["--advanced"])
assert "session-delta" in output


def test_advanced_alone_shows_rebuild() -> None:
_, output = _advanced_help(["--advanced"])
assert "rebuild" in output


def test_advanced_alone_shows_bench() -> None:
_, output = _advanced_help(["--advanced"])
assert "bench" in output


def test_advanced_alone_shows_at_least_four_hidden_subcommands() -> None:
_, output = _advanced_help(["--advanced"])
found = [cmd for cmd in _ADVANCED_SUBCOMMANDS if cmd in output]
assert len(found) >= 4, f"Only found: {found}"


# ---------------------------------------------------------------------------
# 3. `aelf --help --advanced` behaves identically to `aelf --advanced`
# ---------------------------------------------------------------------------


def test_help_advanced_exits_zero() -> None:
code, _ = _advanced_help(["--help", "--advanced"])
assert code == 0


def test_help_advanced_shows_project_warm() -> None:
_, output = _advanced_help(["--help", "--advanced"])
assert "project-warm" in output


def test_help_advanced_shows_session_delta() -> None:
_, output = _advanced_help(["--help", "--advanced"])
assert "session-delta" in output


def test_help_advanced_output_matches_advanced_alone() -> None:
_, out_alone = _advanced_help(["--advanced"])
_, out_with_help = _advanced_help(["--help", "--advanced"])
assert out_alone == out_with_help


# ---------------------------------------------------------------------------
# 4. Advanced-reversed order `aelf --advanced --help` also works
# ---------------------------------------------------------------------------


def test_advanced_help_reversed_order_exits_zero() -> None:
code, _ = _advanced_help(["--advanced", "--help"])
assert code == 0


def test_advanced_help_reversed_order_shows_hidden_subcommands() -> None:
_, output = _advanced_help(["--advanced", "--help"])
assert "project-warm" in output
assert "session-delta" in output
Loading