252 lines
7.4 KiB
Python
252 lines
7.4 KiB
Python
# SPDX-FileCopyrightText: 2026 Art Sea LLC
|
|
#
|
|
# SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
from pathlib import Path
|
|
|
|
from band_aider.ui.startup_wizard import StartupWizard
|
|
|
|
|
|
def test_startup_wizard_uses_vertical_navigation_and_horizontal_toggles() -> None:
|
|
wizard = StartupWizard(
|
|
initial_repo_root=None,
|
|
initial_task=None,
|
|
initial_resume=False,
|
|
initial_overflow_strategy="miser",
|
|
initial_language="python",
|
|
)
|
|
wizard._supported_languages = ["python", "ruby"]
|
|
|
|
wizard._move_step(1)
|
|
assert wizard._step == 1
|
|
|
|
wizard._adjust_current_option(1)
|
|
assert wizard._mode_resume is True
|
|
|
|
wizard._move_step(1)
|
|
wizard._adjust_current_option(1)
|
|
assert wizard._overflow_strategy == "jacker"
|
|
|
|
wizard._move_step(1)
|
|
wizard._adjust_current_option(1)
|
|
assert wizard._language == "ruby"
|
|
|
|
|
|
def test_startup_wizard_marks_selected_item_with_star() -> None:
|
|
wizard = StartupWizard(
|
|
initial_repo_root=Path("/tmp/repo"),
|
|
initial_task="fix it",
|
|
initial_resume=False,
|
|
initial_overflow_strategy="miser",
|
|
initial_language="python",
|
|
)
|
|
wizard._step = 2
|
|
|
|
lines = wizard._prompt_lines(120)
|
|
|
|
assert "* Step 3/5 Escalation: MISER" in lines
|
|
assert " Step 2/5 Mode: New Task" in lines
|
|
assert all("Up/Down toggles" not in line for line in lines)
|
|
|
|
|
|
def test_startup_wizard_footer_changes_with_selected_field() -> None:
|
|
wizard = StartupWizard(
|
|
initial_repo_root=None,
|
|
initial_task=None,
|
|
initial_resume=False,
|
|
initial_overflow_strategy="miser",
|
|
initial_language="python",
|
|
)
|
|
|
|
wizard._step = 0
|
|
assert wizard._footer_text() == (
|
|
"[Up/Down] Move [Type] Edit Path [Backspace] Delete "
|
|
"[Enter] Continue/Start [Esc] Quit"
|
|
)
|
|
|
|
wizard._step = 2
|
|
assert wizard._footer_text() == (
|
|
"[Up/Down] Move [Left/Right] Toggle Escalation "
|
|
"[Enter] Continue/Start [Esc] Quit"
|
|
)
|
|
|
|
|
|
def test_startup_wizard_final_enter_revalidates_repo_root() -> None:
|
|
wizard = StartupWizard(
|
|
initial_repo_root=None,
|
|
initial_task="fix it",
|
|
initial_resume=False,
|
|
initial_overflow_strategy="miser",
|
|
initial_language="python",
|
|
)
|
|
wizard._step = 4
|
|
|
|
assert wizard._on_enter() is False
|
|
assert wizard._step == 0
|
|
assert wizard._message == "Repository root is required."
|
|
|
|
|
|
def test_startup_wizard_uses_authoritative_resume_validation(tmp_path) -> None:
|
|
"""Verify resume cannot start when the project adapter reports no checkpoint."""
|
|
|
|
wizard = StartupWizard(
|
|
initial_repo_root=tmp_path,
|
|
initial_task=None,
|
|
initial_resume=True,
|
|
initial_overflow_strategy="miser",
|
|
initial_language="python",
|
|
resume_available=lambda root: False,
|
|
)
|
|
wizard._step = 4
|
|
|
|
assert wizard._on_enter() is False
|
|
assert wizard._step == 1
|
|
assert "No saved Band-Aider state" in wizard._message
|
|
|
|
|
|
def test_startup_wizard_accepts_authoritative_resume_checkpoint(tmp_path) -> None:
|
|
"""Verify resume starts when the project adapter reports a checkpoint."""
|
|
|
|
checked: list[Path] = []
|
|
wizard = StartupWizard(
|
|
initial_repo_root=tmp_path,
|
|
initial_task=None,
|
|
initial_resume=True,
|
|
initial_overflow_strategy="miser",
|
|
initial_language="python",
|
|
resume_available=lambda root: checked.append(root) is None,
|
|
)
|
|
wizard._step = 4
|
|
|
|
assert wizard._on_enter() is True
|
|
assert checked == [tmp_path]
|
|
|
|
|
|
def test_startup_wizard_final_resume_starts_without_task() -> None:
|
|
"""A validated resume reaches the terminal step without requiring task text."""
|
|
|
|
root = Path("/testbed")
|
|
wizard = StartupWizard(
|
|
initial_repo_root=root,
|
|
initial_task=None,
|
|
initial_resume=True,
|
|
resume_available=lambda candidate: candidate == root,
|
|
)
|
|
wizard._step = 4
|
|
|
|
assert wizard._on_enter() is True
|
|
|
|
|
|
def test_startup_wizard_advances_a_complete_new_task_without_terminal_io() -> None:
|
|
"""The five owned steps accept a complete stationary new-task configuration."""
|
|
|
|
wizard = StartupWizard(
|
|
initial_repo_root=Path("/testbed"),
|
|
initial_task="Repair the widget contract.",
|
|
initial_resume=False,
|
|
initial_overflow_strategy="miser",
|
|
initial_language="python",
|
|
)
|
|
|
|
assert wizard._on_enter() is False
|
|
assert wizard._step == 1
|
|
assert wizard._on_enter() is False
|
|
assert wizard._step == 2
|
|
assert wizard._on_enter() is False
|
|
assert wizard._step == 3
|
|
assert wizard._on_enter() is False
|
|
assert wizard._step == 4
|
|
assert wizard._on_enter() is True
|
|
|
|
|
|
def test_startup_wizard_keeps_resume_unavailable_when_its_authority_fails() -> None:
|
|
"""An unavailable resume authority fails closed and redirects to mode selection."""
|
|
|
|
def unavailable(_root: Path) -> bool:
|
|
"""Represent a stationary checkpoint authority failure."""
|
|
|
|
raise RuntimeError("checkpoint authority unavailable")
|
|
|
|
wizard = StartupWizard(
|
|
initial_repo_root=Path("/testbed"),
|
|
initial_task=None,
|
|
initial_resume=True,
|
|
resume_available=unavailable,
|
|
)
|
|
wizard._step = 4
|
|
|
|
assert wizard._on_enter() is False
|
|
assert wizard._step == 1
|
|
assert "No saved Band-Aider state" in wizard._message
|
|
|
|
|
|
def test_startup_wizard_edits_only_the_active_new_task_field() -> None:
|
|
"""Text entry and backspace are owned by the selected editable field only."""
|
|
|
|
wizard = StartupWizard(
|
|
initial_repo_root=Path("/testbed"),
|
|
initial_task="repair",
|
|
initial_resume=False,
|
|
)
|
|
wizard._step = 4
|
|
|
|
wizard._handle_text("!")
|
|
wizard._handle_backspace()
|
|
assert wizard._task == "repair"
|
|
|
|
wizard._mode_resume = True
|
|
wizard._handle_text("x")
|
|
wizard._handle_backspace()
|
|
assert wizard._task == "repair"
|
|
|
|
|
|
def test_startup_wizard_edits_the_repository_field_and_cycles_unknown_options() -> None:
|
|
"""Repository text is editable at step one and option cycling has safe fallbacks."""
|
|
|
|
wizard = StartupWizard(
|
|
initial_repo_root=None,
|
|
initial_task=None,
|
|
initial_resume=False,
|
|
)
|
|
|
|
wizard._handle_text("/")
|
|
wizard._handle_text("x")
|
|
wizard._handle_backspace()
|
|
assert wizard._repo_root_text == "/"
|
|
assert StartupWizard._cycle_value("unknown", ("miser", "jacker"), 1) == "jacker"
|
|
assert StartupWizard._cycle_value("fixed", (), -1) == "fixed"
|
|
|
|
|
|
def test_startup_wizard_requires_nonblank_new_task_at_the_terminal_step() -> None:
|
|
"""A configured project cannot begin a new run without task evidence."""
|
|
|
|
wizard = StartupWizard(
|
|
initial_repo_root=Path("/testbed"),
|
|
initial_task=" ",
|
|
initial_resume=False,
|
|
)
|
|
wizard._step = 4
|
|
|
|
assert wizard._on_enter() is False
|
|
assert wizard._step == 4
|
|
assert wizard._message == "Task is required for New Task mode."
|
|
|
|
|
|
def test_startup_wizard_renders_resume_specific_prompt_and_footer_copy() -> None:
|
|
"""Resume presentation omits task editing while retaining its no-task explanation."""
|
|
|
|
wizard = StartupWizard(
|
|
initial_repo_root=Path("/testbed"),
|
|
initial_task=None,
|
|
initial_resume=True,
|
|
initial_overflow_strategy="jacker",
|
|
initial_language="python",
|
|
)
|
|
wizard._step = 4
|
|
|
|
prompt = "\n".join(wizard._prompt_lines(28))
|
|
|
|
assert "Step 5/5 Ready: [resume has no task input]" in prompt
|
|
assert "[Type] Edit Task" not in wizard._footer_text()
|
|
wizard._step = 3
|
|
assert "[Left/Right] Change Language" in wizard._footer_text()
|