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
76 changes: 75 additions & 1 deletion java_codebase_rag/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,66 @@ def select_hosts(*, non_interactive: bool, cli_agents: list[str] | None) -> list
return [HOSTS[name] for name in selected]


def select_microservices(
java_dirs: list[Path],
*,
non_interactive: bool,
preselected: list[str] | None = None,
) -> list[str] | None:
"""Show an interactive checklist of detected microservices, all pre-checked.

Returns None when all are selected (-> microservice_roots omitted, index
everything) or a non-empty subset list. Never returns [].

Args:
java_dirs: Detected module roots (relative Path names) from
detect_java_directories. Caller must pass len >= 2.
non_interactive: If True, return None (all) without prompting.
preselected: On re-run, the prior microservice_roots subset to pre-check.
"""
# Defensive guard: caller gates on len >= 2, but stay safe if called directly.
if len(java_dirs) < 2:
return None

dir_names = [str(d) for d in java_dirs]

if non_interactive:
return None

preselected_set = set(preselected) if preselected else None
choices = [
{
"name": name,
"value": name,
"checked": (name in preselected_set) if preselected_set is not None else True,
}
for name in dir_names
]

print("Note: Select which modules to index. Toggle with Space, confirm with Enter.")
selected = prompt(
"checkbox",
"Select microservices to index:",
choices=choices,
default=dir_names, # non-TTY fallback returns all -> caller omits key
)

if not selected:
retry = prompt(
"confirm",
"At least one module is required. Re-select?",
)
if retry:
return select_microservices(java_dirs, non_interactive=False, preselected=preselected)
raise SystemExit(2)

selected_set = set(selected)
if selected_set == set(dir_names):
return None
# Preserve detection order for deterministic YAML output.
return [name for name in dir_names if name in selected_set]


def select_scope(*, non_interactive: bool, cli_scope: str | None) -> Scope:
"""Select 'project' or 'user' scope.

Expand Down Expand Up @@ -1285,6 +1345,20 @@ def run_install(
except SystemExit as e:
return e.code

# Stage 1 (Case B): interactive microservice selection (only when 2+ detected)
try:
selected_roots = (
select_microservices(
java_dirs,
non_interactive=non_interactive,
preselected=existing_config.get("microservice_roots") if existing_config else None,
)
if len(java_dirs) >= 2
else None
)
except SystemExit as e:
return e.code

# Stage 2: Embedding model
resolved_model = resolve_model(model, non_interactive=non_interactive)

Expand Down Expand Up @@ -1327,7 +1401,7 @@ def run_install(
yaml_content = generate_yaml_config(
source_root,
resolved_model,
microservice_roots=[str(d) for d in java_dirs] if len(java_dirs) > 1 else None,
microservice_roots=selected_roots,
existing_yaml=existing_config,
)

Expand Down
83 changes: 83 additions & 0 deletions tests/test_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,89 @@ def test_detect_java_no_root_no_services_exit_2(self, tmp_path, capsys):
assert "Error:" in captured.out and "No Java build files" in captured.out


class TestSelectMicroservices:
"""Test select_microservices function."""

def test_select_microservices_non_interactive_returns_none(self):
"""non_interactive=True with 3 dirs → returns None (all)"""
from java_codebase_rag.installer import select_microservices
dirs = [Path("service-a"), Path("service-b"), Path("service-c")]
result = select_microservices(dirs, non_interactive=True)
assert result is None

def test_select_microservices_non_tty_returns_none_all_selected(self, monkeypatch):
"""non-TTY → prompt returns default (all) → returns None"""
from java_codebase_rag.installer import select_microservices
dirs = [Path("service-a"), Path("service-b"), Path("service-c")]
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
result = select_microservices(dirs, non_interactive=False)
assert result is None

def test_select_microservices_subset_returns_list(self, monkeypatch):
"""prompt checkbox returns ['service-a'] of 3 → returns ['service-a']"""
from java_codebase_rag.installer import select_microservices
dirs = [Path("service-a"), Path("service-b"), Path("service-c")]

def fake_prompt(ptype, message, **kw):
return ["service-a"] if ptype == "checkbox" else True

monkeypatch.setattr("java_codebase_rag.installer.prompt", fake_prompt)
result = select_microservices(dirs, non_interactive=False)
assert result == ["service-a"]

def test_select_microservices_all_selected_returns_none(self, monkeypatch):
"""prompt returns all 3 → returns None"""
from java_codebase_rag.installer import select_microservices
dirs = [Path("service-a"), Path("service-b"), Path("service-c")]
all_names = ["service-a", "service-b", "service-c"]

def fake_prompt(ptype, message, **kw):
return all_names if ptype == "checkbox" else True

monkeypatch.setattr("java_codebase_rag.installer.prompt", fake_prompt)
result = select_microservices(dirs, non_interactive=False)
assert result is None

def test_select_microservices_empty_then_decline_exit_2(self, monkeypatch):
"""prompt checkbox [] + confirm False → SystemExit(2)"""
from java_codebase_rag.installer import select_microservices
dirs = [Path("service-a"), Path("service-b"), Path("service-c")]

def fake_prompt(ptype, message, **kw):
return [] if ptype == "checkbox" else False

monkeypatch.setattr("java_codebase_rag.installer.prompt", fake_prompt)
with pytest.raises(SystemExit) as exc_info:
select_microservices(dirs, non_interactive=False)
assert exc_info.value.code == 2

def test_select_microservices_preselected_marks_choices(self, monkeypatch):
"""preselected=['service-a'] → only service-a has checked=True, result == ['service-a']"""
from java_codebase_rag.installer import select_microservices
dirs = [Path("service-a"), Path("service-b"), Path("service-c")]
captured = {}

def fake_prompt(ptype, message, **kw):
if ptype == "checkbox":
captured["choices"] = kw["choices"]
return ["service-a"]
return True

monkeypatch.setattr("java_codebase_rag.installer.prompt", fake_prompt)
result = select_microservices(dirs, non_interactive=False, preselected=["service-a"])

checked_names = [c["name"] for c in captured["choices"] if c["checked"]]
assert checked_names == ["service-a"]
assert result == ["service-a"]

def test_select_microservices_single_dir_returns_none(self):
"""len(java_dirs) < 2 → returns None"""
from java_codebase_rag.installer import select_microservices
dirs = [Path(".")]
result = select_microservices(dirs, non_interactive=False)
assert result is None


class TestConfirmSourceRoot:
"""Test confirm_source_root function."""

Expand Down
Loading