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
19 changes: 5 additions & 14 deletions src/ucode/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1764,7 +1764,11 @@ def claude_router_hook_cmd(


def _auto_configure_tool(tool: str, custom_oauth: CustomOAuthConfig | None = None) -> None:
"""First-time setup for a single tool — mirrors configure_workspace_command."""
"""Configure a tool for launch without sending a separate validation prompt.

The real agent session follows immediately; explicit configure retains the
test-prompt validation.
"""
existing = load_state()
workspace = existing.get("workspace")
profile = existing.get("profile")
Expand All @@ -1787,19 +1791,6 @@ def _auto_configure_tool(tool: str, custom_oauth: CustomOAuthConfig | None = Non
)
)

with spinner(f"Validating {spec['display']}..."):
ok, err = validate_tool(tool)
if ok:
print_success(f"{spec['display']} is working")
else:
print_err(f"{spec['display']}: {provider_permission_error(tool, state, err)}")
managed = bool(state.get("managed_configs", {}).get(tool))
restore_file(spec["config_path"], spec["backup_path"], managed)
available_tools = [t for t in (state.get("available_tools") or []) if t != tool]
state["available_tools"] = available_tools
save_state(state)
raise RuntimeError(f"{spec['display']} validation failed — config reverted.")


CAN_USE_CACHED_CONFIG_AGENTS = frozenset({"claude", "codex"})

Expand Down
44 changes: 39 additions & 5 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1845,6 +1845,38 @@ def test_reports_runtime_error(self):


class TestAutoConfigureOnFirstRun:
@pytest.mark.parametrize("tool", list(cli_mod.TOOL_SPECS))
@pytest.mark.parametrize("has_workspace", [False, True])
def test_launch_autoconfigures_without_test_prompt(self, tool, has_workspace):
initial_state = {**MINIMAL_STATE, "available_tools": []} if has_workspace else {}
configured_state = {**MINIMAL_STATE, "available_tools": [tool]}
with (
patch("ucode.cli.ensure_bootstrap_dependencies"),
patch("ucode.cli.load_state", return_value=initial_state),
patch(
"ucode.cli._prompt_for_configuration",
return_value=(MINIMAL_STATE["workspace"], None),
),
patch("ucode.cli.configure_shared_state", return_value=configured_state),
patch(
"ucode.cli.configure_single_tool", return_value=configured_state
) as mock_configure,
patch("ucode.cli.ensure_provider_state", return_value=configured_state),
patch("ucode.cli._fetch_managed_config", return_value=(None, False)),
patch("ucode.cli.configure_tool", return_value=configured_state),
patch("ucode.cli.validate_tool", return_value=(False, "timed out")) as mock_validate,
patch("ucode.cli.restore_file") as mock_restore,
patch("ucode.cli.launch_agent") as mock_launch,
):
result = runner.invoke(app, [tool])

assert result.exit_code == 0, result.output
mock_configure.assert_called_once_with(tool, configured_state)
mock_validate.assert_not_called()
mock_restore.assert_not_called()
mock_launch.assert_called_once()
assert mock_launch.call_args.args[:2] == (tool, configured_state)

def test_triggers_when_no_workspace(self):
"""Auto-configure runs when state has no workspace."""
empty_state = {}
Expand Down Expand Up @@ -3205,7 +3237,9 @@ def test_skip_validate_skips_agent_validation(self, monkeypatch):
assert result == 0
assert validated == []

def test_skip_validate_skips_single_tool_validation(self, monkeypatch):
@pytest.mark.parametrize("skip_validate", [False, True])
@pytest.mark.parametrize("tool", list(cli_mod.TOOL_SPECS))
def test_single_tool_validation_is_optional(self, monkeypatch, skip_validate, tool):
import ucode.cli as cli_mod

state = {**MINIMAL_STATE, "workspace": "https://first.com"}
Expand All @@ -3221,16 +3255,16 @@ def test_skip_validate_skips_single_tool_validation(self, monkeypatch):
monkeypatch.setattr(cli_mod, "validate_tool", lambda t: validated.append(t) or (True, ""))

result = cli_mod.configure_workspace_command(
"claude",
tool,
workspaces=[("https://first.com", None)],
skip_validate=True,
skip_validate=skip_validate,
)

assert result == 0
assert validated == []
assert validated == ([] if skip_validate else [tool])
# `ucode configure` (single-agent) still installs AI Tools — it's the
# configure path, unlike launch which auto-configures without installing.
assert installed == [["claude"]]
assert installed == [[tool]]


class TestConfigureSharedStateMcpCleanup:
Expand Down
Loading