diff --git a/src/ucode/cli.py b/src/ucode/cli.py index dee12f1e..5acd272e 100644 --- a/src/ucode/cli.py +++ b/src/ucode/cli.py @@ -70,10 +70,8 @@ render_budget_panel, ) from ucode.managed_config import ( - MANAGED_CONFIG_ENV_VAR, get_model_recommendation, load_managed_state, - managed_agent_config_enabled, refresh_managed_config, ) from ucode.managed_resolve import ( @@ -265,119 +263,6 @@ def _confirm_managed_config_applied(managed: dict, workspace: str) -> None: print_note("Run `ug` to launch with your managed settings.") -def _resolve_workspace_then_maybe_reject( - workspace_entries: list[tuple[str, str | None]] | None, -) -> list[tuple[str, str | None]] | None: - """Resolve the workspace ``ug configure`` targets, then branch on role + managed config. - - Enablement is both client- and server-side: the client-side ``ENABLE_MANAGED_AGENT_CONFIG`` env - var must be set for ``ucode`` to run any of this (the opt-in bug-bash gate below), and the - workspace's gateway must not report the feature disabled (``FEATURE_DISABLED``) — a config only - exists to adopt when the server side is on too. - - When managed coding-agent configs are enabled, ``ug configure`` must still let a developer - switch workspaces — so resolve the target workspace up front (prompting when the interactive - path gave no ``--workspaces``/``--profiles``) and make it current *before* deciding what to do. - Then, gated by the client-side ``ENABLE_MANAGED_AGENT_CONFIG``, the four role/config paths are: - - * **No managed config** → a workspace admin is dropped straight into the ``ug setup`` - authoring flow (``configure`` is replacing ``setup``) and the command exits with its code; a - non-admin's own ``configure`` proceeds, with the resolved entries returned so the caller - reuses them instead of re-prompting. - * **Managed config, non-admin** (or admin status unverifiable) → they're already set: the - launch path applies the config on every ``ucode`` run, so just show it and point them there. - * **Managed config, admin** → drop into the setup flow, whose existing-config menu lets them - adopt it (the same "you're all set" confirmation), re-author it, or delete it; the command exits. - - Without the client-side flag set it returns ``workspace_entries`` unchanged and prompts nothing. - """ - if not managed_agent_config_enabled(): - return workspace_entries - entries = workspace_entries or [_prompt_for_configuration(None)] - workspace, profile = entries[0] - set_current_workspace(workspace) - ensure_databricks_auth(workspace, profile) - # Fetch, don't just read the local cache: on a fresh machine (or right after a reinstall) the - # cache is empty until the first launch, so a cache read would miss a config the workspace does - # publish and wrongly fall through to the local configure flow. `refresh_managed_config` reaches - # the workspace and never raises — it falls back to the persisted copy, then None, on failure. - with spinner("Loading..."): - managed, coding_agent_config_feature_disabled = refresh_managed_config( - {"workspace": workspace, "profile": profile} - ) - if not managed: - if not coding_agent_config_feature_disabled: - _maybe_run_admin_setup(workspace, profile) - return entries - is_admin: bool | None = None - try: - token = get_databricks_token(workspace, profile) - except RuntimeError: - token = None - if token is not None: - with spinner("Checking your workspace permissions..."): - is_admin = is_workspace_admin(workspace, token) - if is_admin: - _run_setup_and_exit(workspace, profile, token) - _confirm_managed_config_applied(managed, workspace) - raise typer.Exit(0) - - -def _maybe_run_admin_setup(workspace: str, profile: str | None) -> None: - """When a workspace admin runs ``configure`` on a workspace with no managed config, drop straight - into the ``ug setup`` authoring flow — ``configure`` is replacing ``setup``, so the admin - never has to invoke it themselves. On completion, exit with setup's own status code. - - A plain developer (and any caller whose admin status can't be verified) instead falls through to - the normal local-configure flow — this function just returns for them. The admin check is - best-effort: any failure to determine admin status (auth or SCIM unreachable) silently skips - setup and returns, so a developer is never blocked behind an authoring flow they can't complete. - """ - try: - token = get_databricks_token(workspace, profile) - except RuntimeError: - return - with spinner("Checking your workspace permissions..."): - is_admin = is_workspace_admin(workspace, token) - if not is_admin: - return - print_note( - "You're a workspace admin, and no managed coding agent config exists for this workspace " - "yet — let's set one up. Choose the agents, models, MCPs, and skills once and every " - "developer inherits them when they run `ug`." - ) - _run_setup_and_exit(workspace, profile, token) - - -def _run_setup_and_exit(workspace: str, profile: str | None, token: str | None = None) -> None: - """Launch the ``ug setup`` authoring flow in place, then exit with its status code. - - Reuses the workspace/profile ``configure`` already resolved and authenticated against so setup - doesn't prompt for them again, and hands setup the same ``token`` the admin check already used - so setup's admin gate can't disagree with the routing decision (e.g. right after a credential - switch, where a second token fetch could resolve a different identity). ``setup_command`` handles - an already-existing config (offering to adopt or edit it). Its actionable failures and aborts are - mapped to clean exit codes rather than bubbling up as unhandled errors. - """ - try: - # Brand the flow as "Configure unity-gateway CLI": it was reached through - # `ug configure`, not a bare `ug setup`, so its section headers use the product - # name rather than the bare command. - code = setup_command( - workspace=workspace, - profile=profile, - command_label="Configure unity-gateway CLI", - token=token, - ) - except RuntimeError as exc: - print_err(str(exc)) - raise typer.Exit(1) from None - except KeyboardInterrupt: - print_err("Interrupted.") - raise typer.Exit(130) from None - raise typer.Exit(code or 0) - - def _print_discovery_diagnostics(state: dict) -> None: """Surface per-source reasons after a failed discovery so the user knows which API call returned what — instead of the generic 'no agents' line.""" @@ -1012,12 +897,7 @@ def status() -> int: if profile: print_kv("CLI profile", profile) - # When the workspace publishes a managed config and this run has the feature switched on, that - # admin-authored config is what launches actually apply — so surface the whole setup as one box - # here too, rather than leaving a developer to infer it from the per-agent rows below. Read from - # the local cache (no network): status is a quick, offline-safe glance, and the cache is what the - # last launch persisted for this workspace. - if workspace and managed_agent_config_enabled(): + if workspace: managed = load_managed_state(workspace) if managed: _print_managed_summary(managed, state, None) @@ -1800,14 +1680,11 @@ def _reject_disabled_agent(managed: dict | None, tool: str) -> None: def _fetch_managed_config(state: dict) -> tuple[dict | None, bool]: - """The workspace's managed config for this launch, or ``(None, _)`` when there is none. + """The workspace's managed config for this launch, plus whether the feature is disabled. - Returns ``(None, False)`` when managed configs are switched off — either the feature is disabled - or the launch passed ``--skip-managed-config`` (which clears the enabling env var for the process). + ``(None, True)`` when the workspace has the feature disabled server-side; ``(None, False)`` when + the feature is on but no config is published. """ - - if not managed_agent_config_enabled(): - return None, False with spinner("Loading..."): return refresh_managed_config(state) @@ -1975,47 +1852,6 @@ def _apply_managed_skills(managed: dict, tool: str, state: dict) -> None: _download_managed_skills(managed, state) -def _can_launch_from_cached_config( - tool: str, - state: dict, - *, - refresh: bool, - model: str | None, - explicit_provider: str | None, - workspace_url: str | None, -) -> bool: - """Return whether a normal Claude/Codex launch can use its cached config.""" - if tool not in CAN_USE_CACHED_CONFIG_AGENTS: - return False - - if refresh or model or explicit_provider is not None: - return False - - if tool == "codex" and smart_routing_v2.enabled(): - if not state.get("codex_models") or not state.get("oss_models"): - return False - - # If managed agent config is enabled, we cannot use the cached state in case the config changed. - if managed_agent_config_enabled(): - return False - - # `_launch_tool` selects an explicit workspace before loading state. A matching workspace here - # therefore means its cached state was selected (or it was just auto-configured) and is safe to - # launch. Keep rejecting a mismatched state rather than launching against the wrong workspace. - if workspace_url is not None and state.get("workspace") != normalize_workspace_url( - workspace_url - ): - return False - - if tool == "claude": - return ( - claude_agent.CLAUDE_SETTINGS_PATH.exists() - and claude_agent.managed_settings_are_current(state) - and claude_agent.gateway_model_discovery_setting_is_absent() - ) - return codex_agent.has_ucode_config() and codex_agent.managed_config_is_current(state) - - def _launch_tool( tool_name: str, ctx: typer.Context, @@ -2062,26 +1898,13 @@ def _launch_tool( # back to whatever `ug configure` saved for this tool. provider = provider or get_provider_service(state, tool) state = _migrate_legacy_smart_routing(state) - if _can_launch_from_cached_config( - tool, - state, - refresh=refresh, - model=model, - explicit_provider=explicit_provider, - workspace_url=workspace_url, - ): - print_section(_launch_title(tool)) - if forwarded_model: - print_kv("Model", forwarded_model) - print_success(f"Starting {TOOL_SPECS[tool]['display']}") - launch_agent(tool, state, ctx.args) - return # Fetched before `configure_shared_state` because it decides whether this agent may launch # at all and whether the model discovery below can be skipped. # Bare `ucode` already fetched one to choose the agent; refetching would double the # control-plane round trip and any fallback warning it printed. + coding_agent_config_feature_disabled = False if managed is None: - managed, _coding_agent_config_feature_disabled = _fetch_managed_config(state) + managed, coding_agent_config_feature_disabled = _fetch_managed_config(state) # Checked before discovery, which can take tens of seconds, so a blocked launch fails fast. _reject_disabled_agent(managed, tool) # Discovery exists to find models and isn't needed for managed config that already names them. @@ -2114,7 +1937,7 @@ def _launch_tool( f"Your workspace's managed config lists no {TOOL_SPECS[tool]['display']}-servable " f"models ({', '.join(unservable)}); using your discovered models instead." ) - elif managed_agent_config_enabled(): + elif not coding_agent_config_feature_disabled: print_note("No managed coding agent config found; using your own settings") if managed is not None: managed_provider = managed_provider_service(managed, tool) @@ -2269,8 +2092,8 @@ def _launch_tool( _print_budget_panel(recommendation, tool, managed) # Register the managed config's MCP servers so they reach the agent's `/mcp` list. Nothing # else on this path does it — the config only lists them — so without this a - # workspace-published server never shows up. `managed` is already None when the config is - # skipped (--skip-managed-config / feature off); --dry-run writes nothing. + # workspace-published server never shows up. `managed` is already None when there is no + # config or the feature is off; --dry-run writes nothing. if managed is not None and not is_dry_run(): _register_managed_mcp_servers(managed, tool, state) _apply_managed_skills(managed, tool, state) @@ -2297,8 +2120,7 @@ def _launch_tool( # Launch-only escape hatch for managed/headless launchers (e.g. omnigent) that # have already run `ug configure`: skip the ~5-10s per-launch auth + AI # Gateway re-validation. Distinct from the configure-only `--skip-validate`, -# which skips the model smoke test, and from `--skip-managed-config`, which -# controls whether the workspace's managed config is applied. +# which skips the model smoke test. SkipPreflightOption = Annotated[ bool, typer.Option( @@ -2313,32 +2135,6 @@ def _launch_tool( "launching." ) -# Ignore the workspace's managed coding-agent config for this one command, on both -# `ug configure` and the launchers. Accepted (and no-op) even when the managed-config -# feature is off, so a headless launcher can always pass it. -SkipManagedConfigOption = Annotated[ - bool, - typer.Option( - "--skip-managed-config", - help="Ignore your workspace's managed coding-agent config for this run, as if managed " - "configs were switched off — use your own local settings instead.", - hidden=True, - ), -] - - -def _disable_managed_config_if_requested(skip_managed_config: bool) -> None: - """Make this process behave as though ``ENABLE_MANAGED_AGENT_CONFIG`` were never set. - - ``managed_agent_config_enabled()`` reads the env var live and gates every managed-config path - (the launch fetch/apply, the budget read, MCP registration, the bare-``ucode`` agent picker, and - the ``configure`` reject-under-managed flow), so clearing it once here short-circuits them all - without threading a flag through each. Per-invocation only: it affects just the current command. - """ - if skip_managed_config: - os.environ.pop(MANAGED_CONFIG_ENV_VAR, None) - - # Target this launch at a specific workspace, auto-configuring (and logging in) # if it hasn't been set up yet — so a launch needs no prior `ug configure`. WorkspaceOption = Annotated[ @@ -2373,7 +2169,6 @@ def default( ), ] = False, skip_preflight: SkipPreflightOption = False, - skip_managed_config: SkipManagedConfigOption = False, workspace: WorkspaceOption = None, ) -> None: """Configure and launch coding agents through Databricks AI Gateway. @@ -2385,7 +2180,6 @@ def default( if ctx.invoked_subcommand is not None: return set_dry_run(dry_run) - _disable_managed_config_if_requested(skip_managed_config) try: _launch_managed_default( ctx, dry_run=dry_run, skip_preflight=skip_preflight, workspace=workspace @@ -2408,27 +2202,29 @@ def _launch_managed_default( workspace: str | None, ) -> None: """Route bare ``ucode`` by whether the workspace publishes a managed config.""" - if not managed_agent_config_enabled(): - console.print(ctx.get_help()) - return if workspace: set_current_workspace(normalize_workspace_url(workspace)) - install_databricks_cli() state = load_state() current = state.get("workspace") if not current: - raise RuntimeError("No workspace configured. Run `ug configure` first.") + console.print(ctx.get_help()) + return + install_databricks_cli() apply_pat_environment(state) - # --dry-run doesn't fetch, so default the feature-disabled flag rather than leave it unbound. coding_agent_config_feature_disabled = False if dry_run: managed = load_managed_state(current) else: with spinner("Loading..."): managed, coding_agent_config_feature_disabled = refresh_managed_config(state) - if not managed and not coding_agent_config_feature_disabled: - _print_no_managed_config_guidance(current, state.get("profile")) + if coding_agent_config_feature_disabled: + print_note( + "Run `ug configure` to set up your coding agents, then launch one with " + "`ug ` (for example `ug claude`)." + ) + return if not managed: + _print_no_managed_config_guidance(current, state.get("profile")) return # The budget tier can move the org to a cheaper agent, so it outranks the config's # default_agent. Fetched here and handed to _launch_tool so it is read once per launch. @@ -2490,7 +2286,6 @@ def codex_cmd( ), ] = False, skip_preflight: SkipPreflightOption = False, - skip_managed_config: SkipManagedConfigOption = False, workspace: WorkspaceOption = None, enable_smart_routing_flag: Annotated[ bool, @@ -2509,7 +2304,6 @@ def codex_cmd( ] = False, ) -> None: """Launch Codex via Databricks.""" - _disable_managed_config_if_requested(skip_managed_config) if enable_smart_routing_flag and disable_smart_routing_flag: print_err("Use only one of --enable-smart-routing or --disable-smart-routing.") raise typer.Exit(1) @@ -2559,7 +2353,6 @@ def claude_cmd( ), ] = False, skip_preflight: SkipPreflightOption = False, - skip_managed_config: SkipManagedConfigOption = False, workspace: WorkspaceOption = None, enable_model_discovery: Annotated[ bool, @@ -2586,7 +2379,6 @@ def claude_cmd( ] = False, ) -> None: """Launch Claude Code via Databricks.""" - _disable_managed_config_if_requested(skip_managed_config) if enable_smart_routing_flag and disable_smart_routing_flag: print_err("Use only one of --enable-smart-routing or --disable-smart-routing.") raise typer.Exit(1) @@ -2629,10 +2421,8 @@ def gemini_cmd( ), ] = None, skip_preflight: SkipPreflightOption = False, - skip_managed_config: SkipManagedConfigOption = False, ) -> None: """Launch Gemini CLI via Databricks.""" - _disable_managed_config_if_requested(skip_managed_config) _launch_tool("gemini", ctx, provider=provider, model=model, skip_preflight=skip_preflight) @@ -2642,10 +2432,8 @@ def gemini_cmd( def opencode_cmd( ctx: typer.Context, skip_preflight: SkipPreflightOption = False, - skip_managed_config: SkipManagedConfigOption = False, ) -> None: """Launch OpenCode via Databricks.""" - _disable_managed_config_if_requested(skip_managed_config) _launch_tool("opencode", ctx, skip_preflight=skip_preflight) @@ -2653,10 +2441,8 @@ def opencode_cmd( def copilot_cmd( ctx: typer.Context, skip_preflight: SkipPreflightOption = False, - skip_managed_config: SkipManagedConfigOption = False, ) -> None: """Launch GitHub Copilot CLI via Databricks.""" - _disable_managed_config_if_requested(skip_managed_config) _launch_tool("copilot", ctx, skip_preflight=skip_preflight) @@ -2664,10 +2450,8 @@ def copilot_cmd( def pi_cmd( ctx: typer.Context, skip_preflight: SkipPreflightOption = False, - skip_managed_config: SkipManagedConfigOption = False, ) -> None: """Launch Pi coding agent via Databricks.""" - _disable_managed_config_if_requested(skip_managed_config) _launch_tool("pi", ctx, skip_preflight=skip_preflight) @@ -2819,7 +2603,6 @@ def configure( "still applied.", ), ] = False, - skip_managed_config: SkipManagedConfigOption = False, verbose: Annotated[ str, typer.Option( @@ -2832,7 +2615,6 @@ def configure( """Configure workspace URL and AI Gateway.""" if ctx.invoked_subcommand is not None: return - _disable_managed_config_if_requested(skip_managed_config) if verbose not in ("normal", "low"): print_err("--verbose must be one of: normal, low.") raise typer.Exit(2) @@ -2862,14 +2644,7 @@ def configure( workspace_entries = _parse_workspaces_option(workspaces) if workspaces is not None else None if profiles is not None: workspace_entries = _parse_profiles_option(profiles) - # Whether the user named the workspace(s) via flags, captured before the resolver below - # may fill `workspace_entries` from a prompt — this, not the resolved value, decides the - # fully-interactive MCP prompt at the end. flag_driven_workspace = workspace_entries is not None - # Under a managed config, resolve (prompting when interactive) and set the target workspace - # first, so the developer can switch workspaces; only then short-circuit if that workspace - # is already managed. Returns the resolved entries so the flow below doesn't prompt again. - workspace_entries = _resolve_workspace_then_maybe_reject(workspace_entries) # Only forward the opt-in flags when set so existing call expectations # (and defaults) stay unchanged for the common interactive path. skip_kwargs: dict = {} @@ -2983,9 +2758,7 @@ def configure( ) # Only the no-agent, no-workspace path is truly interactive (the user # picked agents/workspace via prompts); that's where we offer the MCP - # step below. Flag-driven runs stay scriptable. Keyed off whether the - # workspace came from a flag, not the now-resolved `workspace_entries` - # (which the managed-config resolver may have filled from a prompt). + # step below. Flag-driven runs stay scriptable. fully_interactive = not flag_driven_workspace if tracing: # The workspaces were just configured, so enable tracing for them diff --git a/src/ucode/managed_config.py b/src/ucode/managed_config.py index cc43a333..9a7d4144 100644 --- a/src/ucode/managed_config.py +++ b/src/ucode/managed_config.py @@ -40,10 +40,6 @@ MANAGED_STATE_PATH = config_io.APP_DIR / "managed-state.json" -# Opt-in switch while the feature is in bug bash: unset means launches ignore managed configs -# entirely and behave exactly as they did before. -MANAGED_CONFIG_ENV_VAR = "ENABLE_MANAGED_AGENT_CONFIG" - # Shown to a developer when their workspace has no admin-defined managed config yet — the normal # case, not an error. Kept here so the CLI (which surfaces it) uses one consistent message. NO_MANAGED_CONFIG_MESSAGE = "No coding-agent config has been set up by your workspace admin yet." @@ -503,11 +499,3 @@ def _summarize_read_failure(reason: str) -> str: return status.strip() condensed = " ".join(reason.split()) return condensed if len(condensed) <= 160 else condensed[:157] + "..." - - -def managed_agent_config_enabled() -> bool: - """True when managed coding-agent configs are switched on for this run. - - Opt-in while the feature is being bug-bashed: without the env var set, launches behave exactly - as they did before and never read the workspace's config.""" - return os.environ.get(MANAGED_CONFIG_ENV_VAR, "").strip().lower() in ("1", "true", "yes") diff --git a/tests/conftest.py b/tests/conftest.py index 2992622a..9d861f23 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -52,9 +52,6 @@ def reject_privileged_write(path, _desired_text): ) monkeypatch.setattr(managed_files_mod, "_sudo_replace", reject_privileged_write) - # Isolate the managed-config opt-in from the developer's own shell: leaving it set changes what - # `ucode`/`ucode configure` do mid-test. Tests that exercise the managed path set it explicitly. - monkeypatch.delenv("ENABLE_MANAGED_AGENT_CONFIG", raising=False) monkeypatch.delenv("ENABLE_CLAUDE_CODE_GATEWAY_MODEL_DISCOVERY", raising=False) monkeypatch.delenv("CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY", raising=False) # The model-services listing is memoized for the life of the process, so without this a cached diff --git a/tests/test_cli.py b/tests/test_cli.py index 80e1c1bd..56fb9d1d 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -481,7 +481,6 @@ def test_codex_forwarded_model_is_reported_in_launch_summary(self): patch("ucode.cli.ensure_bootstrap_dependencies"), patch("ucode.cli.load_state", return_value=state), patch("ucode.cli.ensure_provider_state", return_value=state), - patch("ucode.cli._can_launch_from_cached_config", return_value=False), patch("ucode.cli.configure_shared_state", return_value=state), patch( "ucode.cli.resolve_launch_model", @@ -569,7 +568,6 @@ def test_claude_v2_skips_legacy_prelaunch_routing(self, monkeypatch): patch("ucode.cli.ensure_bootstrap_dependencies"), patch("ucode.cli.load_state", return_value=state), patch("ucode.cli.ensure_provider_state", return_value=state), - patch("ucode.cli._can_launch_from_cached_config", return_value=False), patch("ucode.cli.configure_shared_state", return_value=state), patch( "ucode.cli.resolve_launch_model", @@ -736,7 +734,6 @@ def test_forwarded_model_is_reported_in_launch_summary(self, forwarded_args): patch("ucode.cli.ensure_bootstrap_dependencies"), patch("ucode.cli.load_state", return_value=state), patch("ucode.cli.ensure_provider_state", return_value=state), - patch("ucode.cli._can_launch_from_cached_config", return_value=False), patch("ucode.cli.configure_shared_state", return_value=state), patch( "ucode.cli.resolve_launch_model", @@ -1142,7 +1139,6 @@ def test_status_treats_available_tools_as_configured_agents(self): assert "https://example.databricks.com/ai-gateway/gemini" not in result.output def test_status_shows_managed_config_box_when_present_and_enabled(self, monkeypatch): - monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1") managed = { "enabled_agents": {"claude": {}, "codex": {}}, "mcp_servers": [{"name": "github-mcp", "type": "external"}], @@ -1160,22 +1156,7 @@ def test_status_shows_managed_config_box_when_present_and_enabled(self, monkeypa assert "github-mcp" in result.output assert "debug-ci" in result.output - def test_status_hides_managed_config_box_when_feature_disabled(self, monkeypatch): - monkeypatch.delenv("ENABLE_MANAGED_AGENT_CONFIG", raising=False) - managed = {"enabled_agents": {"claude": {}}} - with ( - patch("ucode.cli.load_state", return_value=MINIMAL_STATE), - patch("ucode.cli.load_managed_state", return_value=managed) as load_managed, - ): - result = runner.invoke(app, ["status"]) - - assert result.exit_code == 0, result.output - assert "Workspace-managed config" not in result.output - # Feature off: the managed cache is never consulted. - load_managed.assert_not_called() - def test_status_hides_managed_config_box_when_none_present(self, monkeypatch): - monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1") with ( patch("ucode.cli.load_state", return_value=MINIMAL_STATE), patch("ucode.cli.load_managed_state", return_value=None), @@ -1629,66 +1610,6 @@ def test_reports_runtime_error(self): class TestAutoConfigureOnFirstRun: - def test_uses_existing_claude_settings_without_preflight(self, tmp_path): - from pathlib import Path - - settings_path = tmp_path / "ucode-settings.json" - settings_path.write_text("{}", encoding="utf-8") - with ( - patch("ucode.cli.ensure_bootstrap_dependencies"), - patch("ucode.cli.load_state", return_value=MINIMAL_STATE), - patch("ucode.cli.ensure_provider_state", return_value=MINIMAL_STATE), - patch("ucode.cli.configure_shared_state") as mock_preflight, - patch("ucode.cli.configure_tool") as mock_configure, - patch("ucode.cli.claude_agent.CLAUDE_SETTINGS_PATH", Path(settings_path)), - patch("ucode.cli.launch_agent") as mock_launch, - ): - result = runner.invoke(app, ["claude"]) - - assert result.exit_code == 0, result.output - mock_preflight.assert_not_called() - mock_configure.assert_not_called() - mock_launch.assert_called_once() - - def test_uses_existing_codex_config_without_preflight(self): - with ( - patch("ucode.cli.ensure_bootstrap_dependencies"), - patch("ucode.cli.load_state", return_value=MINIMAL_STATE), - patch("ucode.cli.ensure_provider_state", return_value=MINIMAL_STATE), - patch("ucode.cli.configure_shared_state") as mock_preflight, - patch("ucode.cli.configure_tool") as mock_configure, - patch("ucode.cli.codex_agent.has_ucode_config", return_value=True), - patch("ucode.cli.install_databricks_ai_tools_for_agents") as mock_ai_tools, - patch("ucode.cli.launch_agent") as mock_launch, - ): - result = runner.invoke(app, ["codex"]) - - assert result.exit_code == 0, result.output - mock_preflight.assert_not_called() - mock_configure.assert_not_called() - mock_ai_tools.assert_not_called() - mock_launch.assert_called_once() - - def test_workspace_flag_uses_cached_codex_config_without_preflight(self): - with ( - patch("ucode.cli.ensure_bootstrap_dependencies"), - patch("ucode.cli.set_current_workspace") as mock_set_workspace, - patch("ucode.cli.load_state", return_value=MINIMAL_STATE), - patch("ucode.cli.ensure_provider_state", return_value=MINIMAL_STATE), - patch("ucode.cli.configure_shared_state") as mock_preflight, - patch("ucode.cli.configure_tool") as mock_configure, - patch("ucode.cli.smart_routing_v2.enabled", return_value=False), - patch("ucode.cli.codex_agent.has_ucode_config", return_value=True), - patch("ucode.cli.launch_agent") as mock_launch, - ): - result = runner.invoke(app, ["codex", "--workspace", "https://example.databricks.com/"]) - - assert result.exit_code == 0, result.output - mock_set_workspace.assert_called_once_with("https://example.databricks.com") - mock_preflight.assert_not_called() - mock_configure.assert_not_called() - mock_launch.assert_called_once() - def test_triggers_when_no_workspace(self): """Auto-configure runs when state has no workspace.""" empty_state = {} @@ -1793,178 +1714,6 @@ def test_cursor_launch_uses_unity_gateway_branding(): assert "Unity Gateway with Cursor" in result.output -class TestCachedConfigPredicate: - @staticmethod - def _kwargs(**overrides): - kwargs = { - "refresh": False, - "model": None, - "explicit_provider": None, - "workspace_url": None, - } - kwargs.update(overrides) - return kwargs - - def test_accepts_configured_codex_launch(self): - import ucode.cli as cli_mod - - with ( - patch("ucode.cli.managed_agent_config_enabled", return_value=False), - patch("ucode.cli.codex_agent.has_ucode_config", return_value=True), - patch("ucode.cli.codex_agent.managed_config_is_current", return_value=True), - ): - assert ( - cli_mod._can_launch_from_cached_config("codex", MINIMAL_STATE, **self._kwargs()) - is True - ) - - with ( - patch("ucode.cli.managed_agent_config_enabled", return_value=False), - patch("ucode.cli.codex_agent.has_ucode_config", return_value=True), - patch("ucode.cli.codex_agent.managed_config_is_current", return_value=False), - ): - assert ( - cli_mod._can_launch_from_cached_config("codex", MINIMAL_STATE, **self._kwargs()) - is False - ) - - def test_accepts_claude_only_when_managed_settings_are_verified(self, tmp_path): - import ucode.cli as cli_mod - - settings_path = tmp_path / "ucode-settings.json" - settings_path.write_text("{}", encoding="utf-8") - with ( - patch("ucode.cli.managed_agent_config_enabled", return_value=False), - patch("ucode.cli.claude_agent.CLAUDE_SETTINGS_PATH", settings_path), - patch("ucode.cli.claude_agent.managed_settings_are_current", return_value=True), - ): - assert ( - cli_mod._can_launch_from_cached_config("claude", MINIMAL_STATE, **self._kwargs()) - is True - ) - - with ( - patch("ucode.cli.managed_agent_config_enabled", return_value=False), - patch("ucode.cli.claude_agent.CLAUDE_SETTINGS_PATH", settings_path), - patch("ucode.cli.claude_agent.managed_settings_are_current", return_value=False), - ): - assert ( - cli_mod._can_launch_from_cached_config("claude", MINIMAL_STATE, **self._kwargs()) - is False - ) - - def test_accepts_codex_v2_launch_with_complete_model_cache(self, monkeypatch): - import ucode.cli as cli_mod - - monkeypatch.setenv("ENABLE_SMART_ROUTING_V2", "1") - state = { - **MINIMAL_STATE, - "codex_models": ["system.ai.gpt-5-6-sol"], - "oss_models": ["system.ai.glm-5-2"], - } - with ( - patch("ucode.cli.managed_agent_config_enabled", return_value=False), - patch("ucode.cli.codex_agent.has_ucode_config", return_value=True), - patch("ucode.cli.codex_agent.managed_config_is_current", return_value=True), - ): - assert cli_mod._can_launch_from_cached_config("codex", state, **self._kwargs()) is True - - @pytest.mark.parametrize( - ("incomplete_key", "value"), - [ - ("codex_models", None), - ("codex_models", []), - ("oss_models", None), - ("oss_models", []), - ], - ) - def test_rejects_codex_v2_launch_with_incomplete_model_cache( - self, monkeypatch, incomplete_key, value - ): - import ucode.cli as cli_mod - - monkeypatch.setenv("ENABLE_SMART_ROUTING_V2", "1") - state = { - **MINIMAL_STATE, - "codex_models": ["system.ai.gpt-5-6-sol"], - "oss_models": ["system.ai.glm-5-2"], - } - if value is None: - state.pop(incomplete_key) - else: - state[incomplete_key] = value - with patch("ucode.cli.managed_agent_config_enabled", return_value=False): - assert cli_mod._can_launch_from_cached_config("codex", state, **self._kwargs()) is False - - @pytest.mark.parametrize( - "override", - [ - {"refresh": True}, - {"explicit_provider": "catalog.schema.provider"}, - {"workspace_url": "https://other.databricks.com"}, - ], - ) - def test_rejects_dynamic_launch_overrides(self, override): - import ucode.cli as cli_mod - - with ( - patch("ucode.cli.managed_agent_config_enabled", return_value=False), - patch("ucode.cli.codex_agent.has_ucode_config", return_value=True), - ): - assert ( - cli_mod._can_launch_from_cached_config( - "codex", MINIMAL_STATE, **self._kwargs(**override) - ) - is False - ) - - -class TestPassthroughArgs: - @pytest.mark.parametrize( - "tool,extra_args", - [ - ("claude", ["-r"]), - ("claude", ["--resume"]), - ("codex", ["--full-auto"]), - ("gemini", ["--debug"]), - ("opencode", ["--model", "my-model"]), - ("claude", ["-r", "--some-flag", "value"]), - ], - ) - def test_extra_args_forwarded(self, tool, extra_args): - patches = _patch_launch(tool) - with ( - patches[0], - patches[1], - patches[2], - patches[3], - patches[4], - patches[5], - patches[6], - patches[7] as mock_launch, - ): - result = runner.invoke(app, [tool, *extra_args]) - assert result.exit_code == 0, result.output - forwarded = mock_launch.call_args[0][2] - assert forwarded == extra_args - - def test_no_extra_args_passes_empty_list(self): - patches = _patch_launch("claude") - with ( - patches[0], - patches[1], - patches[2], - patches[3], - patches[4], - patches[5], - patches[6], - patches[7] as mock_launch, - ): - runner.invoke(app, ["claude"]) - forwarded = mock_launch.call_args[0][2] - assert forwarded == [] - - class TestConfigureAgentFlag: def test_no_flag_calls_configure_all(self): with ( @@ -3448,7 +3197,6 @@ def _patches(cfg): patch("ucode.cli._auto_configure_tool"), patch("ucode.cli.load_state", return_value=MINIMAL_STATE), patch("ucode.cli.ensure_provider_state", return_value=MINIMAL_STATE), - patch("ucode.cli._can_launch_from_cached_config", return_value=False), patch("ucode.cli.configure_shared_state", cfg), patch("ucode.cli.codex_agent.has_ucode_config", return_value=False), patch( @@ -3515,37 +3263,14 @@ def _fetch(state): return cli_mod._fetch_managed_config(state) def test_fetches_fresh_when_enabled(self, monkeypatch): - monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1") monkeypatch.setattr( "ucode.cli.refresh_managed_config", lambda state: ({"enabled_agents": {}}, False) ) assert self._fetch({"workspace": "https://w"}) == ({"enabled_agents": {}}, False) - @pytest.mark.parametrize("env_value", [None, "", "0", "off", "no"]) - def test_disabled_reads_nothing_at_all(self, monkeypatch, env_value): - """While the feature is opt-in, a disabled launch must not read the config or the network.""" - if env_value is None: - monkeypatch.delenv("ENABLE_MANAGED_AGENT_CONFIG", raising=False) - else: - monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", env_value) - for name in ("refresh_managed_config", "load_managed_state"): - monkeypatch.setattr( - f"ucode.cli.{name}", - lambda *a, called=name, **k: pytest.fail(f"{called} must not run when disabled"), - ) - assert self._fetch({"workspace": "https://w"}) == (None, False) - - def test_skip_managed_config_makes_the_fetch_a_no_op(self, monkeypatch): - # --skip-managed-config clears the enabling env var, so the read behaves as feature-off: - # no fetch, no cache read, no network — just None. - monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1") - monkeypatch.setattr( - "ucode.cli.refresh_managed_config", lambda state: pytest.fail("should not fetch") - ) - import ucode.cli as cli_mod - - cli_mod._disable_managed_config_if_requested(True) - assert self._fetch({"workspace": "https://w"}) == (None, False) + def test_feature_disabled_returns_none_and_the_flag(self, monkeypatch): + monkeypatch.setattr("ucode.cli.refresh_managed_config", lambda state: (None, True)) + assert self._fetch({"workspace": "https://w"}) == (None, True) class TestManagedConfigDecidesDiscoveryFromFreshRead: @@ -3556,7 +3281,6 @@ def test_a_removed_model_list_no_longer_skips_discovery(self, monkeypatch): models. Deciding from that cache would skip discovery for a config that no longer supplies models, so the launch would have neither. """ - monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1") stale_cache = { "enabled_agents": {"claude": {"model_config": {"models": {"default_opus_model": "m"}}}} } @@ -3581,372 +3305,6 @@ def test_a_removed_model_list_no_longer_skips_discovery(self, monkeypatch): assert mock_shared.call_args.kwargs["skip_model_discovery"] is False -class TestConfigureDeprecation: - """`ucode configure` resolves the target workspace first, authenticates, then branches on the - caller's role and whether the workspace already publishes a managed config (AIGTWY-4338).""" - - @pytest.fixture(autouse=True) - def _stub_auth(self, monkeypatch): - # The resolver authenticates up front (before the config read and admin check); keep that a - # no-op so these tests never touch a real Databricks login. - monkeypatch.setattr("ucode.cli.ensure_databricks_auth", lambda *a, **k: None) - - @staticmethod - def _resolve(entries=None): - import ucode.cli as cli_mod - - return cli_mod._resolve_workspace_then_maybe_reject(entries) - - @staticmethod - def _stub_admin(monkeypatch, value): - """Stub the best-effort admin check the has-config branch runs (token + SCIM).""" - monkeypatch.setattr("ucode.cli.get_databricks_token", lambda ws, profile=None: "tok") - monkeypatch.setattr("ucode.cli.is_workspace_admin", lambda ws, tok: value) - - @staticmethod - def _stub_setup(monkeypatch): - """Replace ``setup_command`` with a recorder returning exit code 0.""" - setup_calls: list[dict] = [] - monkeypatch.setattr( - "ucode.cli.setup_command", lambda **kwargs: setup_calls.append(kwargs) or 0 - ) - return setup_calls - - def test_non_admin_with_config_confirms_and_exits(self, monkeypatch, capsys): - # A non-admin on a managed workspace has nothing to configure locally — the launch path - # applies the config every run. Configure just shows what's in force and points at `ucode`; - # it must not route into setup or write anything. - import typer - - monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1") - monkeypatch.setattr("ucode.cli.set_current_workspace", lambda ws: None) - monkeypatch.setattr("ucode.cli.load_state", lambda: {"workspace": "https://w"}) - monkeypatch.setattr( - "ucode.cli.refresh_managed_config", - lambda state: ({"enabled_agents": {"claude": {}}}, False), - ) - self._stub_admin(monkeypatch, False) - monkeypatch.setattr( - "ucode.cli.setup_command", - lambda **kwargs: pytest.fail("a non-admin must not be routed into setup"), - ) - with pytest.raises(typer.Exit) as exc: - self._resolve([("https://w", None)]) - assert exc.value.exit_code == 0 - out = capsys.readouterr().out - assert "you're all set" in out - assert "Run `ug`" in out - - def test_fetches_the_config_rather_than_reading_a_cold_cache(self, monkeypatch): - # The gap this guards: on a fresh machine the local cache is empty until the first launch, - # so a cache read would miss a config the workspace does publish. The resolver must fetch. - import typer - - monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1") - monkeypatch.setattr("ucode.cli.set_current_workspace", lambda ws: None) - monkeypatch.setattr("ucode.cli.load_state", lambda: {"workspace": "https://w"}) - # Cold cache — a cache read would wrongly fall through to the local configure flow. - monkeypatch.setattr("ucode.cli.load_managed_state", lambda ws: None) - monkeypatch.setattr( - "ucode.cli.refresh_managed_config", - lambda state: ({"enabled_agents": {"claude": {}}}, False), - ) - self._stub_admin(monkeypatch, False) - with pytest.raises(typer.Exit) as exc: - self._resolve([("https://w", None)]) - assert exc.value.exit_code == 0 - - def test_admin_with_config_runs_setup(self, monkeypatch): - # An admin on a workspace that already has a config is dropped into setup — whose - # existing-config menu offers re-author/delete — rather than the confirm-only path. - import typer - - monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1") - monkeypatch.setattr("ucode.cli.set_current_workspace", lambda ws: None) - monkeypatch.setattr( - "ucode.cli.refresh_managed_config", - lambda state: ({"enabled_agents": {"claude": {}}}, False), - ) - self._stub_admin(monkeypatch, True) - setup_calls = self._stub_setup(monkeypatch) - with pytest.raises(typer.Exit) as exc: - self._resolve([("https://w", None)]) - assert exc.value.exit_code == 0 - assert setup_calls == [ - { - "workspace": "https://w", - "profile": None, - "command_label": "Configure unity-gateway CLI", - "token": "tok", - } - ] - - def test_admin_status_unknown_with_config_confirms_without_setup(self, monkeypatch): - # `is_workspace_admin` returns None when the SCIM check fails; treat as non-admin and take - # the confirm path rather than routing an unverifiable caller into the admin-only setup flow. - import typer - - monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1") - monkeypatch.setattr("ucode.cli.set_current_workspace", lambda ws: None) - monkeypatch.setattr("ucode.cli.load_state", lambda: {"workspace": "https://w"}) - monkeypatch.setattr( - "ucode.cli.refresh_managed_config", - lambda state: ({"enabled_agents": {"claude": {}}}, False), - ) - self._stub_admin(monkeypatch, None) - monkeypatch.setattr( - "ucode.cli.setup_command", - lambda **kwargs: pytest.fail("unknown admin status must not route into setup"), - ) - with pytest.raises(typer.Exit) as exc: - self._resolve([("https://w", None)]) - assert exc.value.exit_code == 0 - - def test_token_failure_with_config_confirms_without_checking_admin(self, monkeypatch): - # A token failure must not block the confirm path for a config the workspace does publish. - import typer - - monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1") - monkeypatch.setattr("ucode.cli.set_current_workspace", lambda ws: None) - monkeypatch.setattr("ucode.cli.load_state", lambda: {"workspace": "https://w"}) - monkeypatch.setattr( - "ucode.cli.refresh_managed_config", - lambda state: ({"enabled_agents": {"claude": {}}}, False), - ) - - def _boom(ws, profile=None): - raise RuntimeError("no token") - - monkeypatch.setattr("ucode.cli.get_databricks_token", _boom) - monkeypatch.setattr( - "ucode.cli.is_workspace_admin", - lambda ws, tok: pytest.fail("admin check needs a token"), - ) - monkeypatch.setattr( - "ucode.cli.setup_command", lambda **kwargs: pytest.fail("no setup without a token") - ) - with pytest.raises(typer.Exit) as exc: - self._resolve([("https://w", None)]) - assert exc.value.exit_code == 0 - - @staticmethod - def _stub_not_admin(monkeypatch): - # No managed config -> the resolver now checks admin status; keep the developer case simple. - monkeypatch.setattr("ucode.cli.get_databricks_token", lambda ws, profile=None: "tok") - monkeypatch.setattr("ucode.cli.is_workspace_admin", lambda ws, tok: False) - - def test_prompts_for_the_workspace_before_checking_the_config(self, monkeypatch): - # The whole point: even under a managed config the developer can still switch workspaces, - # so the prompt runs (and the picked workspace is made current) before the config check. - monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1") - picked = [] - monkeypatch.setattr( - "ucode.cli._prompt_for_configuration", lambda tool=None: ("https://picked", None) - ) - monkeypatch.setattr("ucode.cli.set_current_workspace", lambda ws: picked.append(ws)) - monkeypatch.setattr("ucode.cli.refresh_managed_config", lambda state: (None, False)) - self._stub_not_admin(monkeypatch) - entries = self._resolve(None) - assert picked == ["https://picked"] - assert entries == [("https://picked", None)] - - def test_returns_flag_entries_and_proceeds_without_a_managed_config(self, monkeypatch): - # Setting up a new workspace still goes through `ucode configure`, so hand the resolved - # workspace back to the caller instead of re-prompting. - monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1") - monkeypatch.setattr("ucode.cli.set_current_workspace", lambda ws: None) - monkeypatch.setattr("ucode.cli.refresh_managed_config", lambda state: (None, False)) - self._stub_not_admin(monkeypatch) - entries = self._resolve([("https://w", None)]) - assert entries == [("https://w", None)] - - def test_admin_with_no_config_runs_setup_in_place(self, monkeypatch): - # `configure` is replacing `setup`: an admin on a config-less workspace is dropped straight - # into the setup authoring flow (reusing the resolved workspace/profile) and the command - # exits with setup's own status code — no prompt, no fall-through to the manual flow. - import typer - - monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1") - monkeypatch.setattr("ucode.cli.set_current_workspace", lambda ws: None) - monkeypatch.setattr("ucode.cli.refresh_managed_config", lambda state: (None, False)) - monkeypatch.setattr("ucode.cli.get_databricks_token", lambda ws, profile=None: "tok") - monkeypatch.setattr("ucode.cli.is_workspace_admin", lambda ws, tok: True) - setup_calls: list[dict] = [] - monkeypatch.setattr( - "ucode.cli.setup_command", - lambda **kwargs: setup_calls.append(kwargs) or 0, - ) - with pytest.raises(typer.Exit) as exc: - self._resolve([("https://w", None)]) - assert exc.value.exit_code == 0 - assert setup_calls == [ - { - "workspace": "https://w", - "profile": None, - "command_label": "Configure unity-gateway CLI", - "token": "tok", - } - ] - - def test_setup_failure_maps_to_a_nonzero_exit(self, monkeypatch): - # `setup_command` raises RuntimeError for actionable failures; the resolver surfaces it as a - # clean non-zero exit rather than letting it bubble as an unhandled error. - import typer - - monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1") - monkeypatch.setattr("ucode.cli.set_current_workspace", lambda ws: None) - monkeypatch.setattr("ucode.cli.refresh_managed_config", lambda state: (None, False)) - monkeypatch.setattr("ucode.cli.get_databricks_token", lambda ws, profile=None: "tok") - monkeypatch.setattr("ucode.cli.is_workspace_admin", lambda ws, tok: True) - - def _boom(**kwargs): - raise RuntimeError("no agents available") - - monkeypatch.setattr("ucode.cli.setup_command", _boom) - with pytest.raises(typer.Exit) as exc: - self._resolve([("https://w", None)]) - assert exc.value.exit_code == 1 - - def test_non_admin_with_no_config_falls_through_without_setup(self, monkeypatch): - monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1") - monkeypatch.setattr("ucode.cli.set_current_workspace", lambda ws: None) - monkeypatch.setattr("ucode.cli.refresh_managed_config", lambda state: (None, False)) - monkeypatch.setattr("ucode.cli.get_databricks_token", lambda ws, profile=None: "tok") - monkeypatch.setattr("ucode.cli.is_workspace_admin", lambda ws, tok: False) - monkeypatch.setattr( - "ucode.cli.setup_command", - lambda **kwargs: pytest.fail("a non-admin must not be routed into setup"), - ) - entries = self._resolve([("https://w", None)]) - assert entries == [("https://w", None)] - - def test_admin_check_failure_falls_through_without_setup(self, monkeypatch): - # `is_workspace_admin` returns None when the check itself fails; treat as "not an admin" so - # a developer is never blocked behind an authoring flow they can't complete. - monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1") - monkeypatch.setattr("ucode.cli.set_current_workspace", lambda ws: None) - monkeypatch.setattr("ucode.cli.refresh_managed_config", lambda state: (None, False)) - monkeypatch.setattr("ucode.cli.get_databricks_token", lambda ws, profile=None: "tok") - monkeypatch.setattr("ucode.cli.is_workspace_admin", lambda ws, tok: None) - monkeypatch.setattr( - "ucode.cli.setup_command", - lambda **kwargs: pytest.fail("no setup when admin status is unverifiable"), - ) - entries = self._resolve([("https://w", None)]) - assert entries == [("https://w", None)] - - def test_feature_disabled_server_side_does_not_run_setup(self, monkeypatch): - # The coding-agent-configs feature isn't enabled server-side, so `ucode setup` can't publish. - # Even an admin just falls through to the normal configure flow. - monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1") - monkeypatch.setattr("ucode.cli.set_current_workspace", lambda ws: None) - monkeypatch.setattr("ucode.cli.refresh_managed_config", lambda state: (None, True)) - monkeypatch.setattr("ucode.cli.get_databricks_token", lambda ws, profile=None: "tok") - monkeypatch.setattr("ucode.cli.is_workspace_admin", lambda ws, tok: True) - monkeypatch.setattr( - "ucode.cli.setup_command", - lambda **kwargs: pytest.fail("setup can't publish when the feature is disabled"), - ) - entries = self._resolve([("https://w", None)]) - assert entries == [("https://w", None)] - - def test_configure_command_exits_zero_without_erroring(self, monkeypatch): - # `typer.Exit(0)` subclasses RuntimeError, so the command's own RuntimeError handler must - # not catch the clean exit and print `str(exc)` -> a bare, meaningless "ERROR 0". - monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1") - monkeypatch.setattr("ucode.cli.set_current_workspace", lambda ws: None) - monkeypatch.setattr("ucode.cli.load_state", lambda: {"workspace": "https://w"}) - monkeypatch.setattr( - "ucode.cli.refresh_managed_config", - lambda state: ({"enabled_agents": {"claude": {}}}, False), - ) - self._stub_admin(monkeypatch, False) - with ( - patch("ucode.cli.install_databricks_cli"), - patch("ucode.cli._prompt_for_configuration", return_value=("https://w", None)), - ): - result = runner.invoke(app, ["configure"]) - assert result.exit_code == 0, result.output - assert "ERROR" not in result.output - - @pytest.mark.parametrize("env_value", [None, "", "0"]) - def test_passes_entries_through_when_the_env_var_is_off(self, monkeypatch, capsys, env_value): - if env_value is None: - monkeypatch.delenv("ENABLE_MANAGED_AGENT_CONFIG", raising=False) - else: - monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", env_value) - monkeypatch.setattr( - "ucode.cli.load_managed_state", - lambda ws: pytest.fail("must not read the config when disabled"), - ) - monkeypatch.setattr( - "ucode.cli._prompt_for_configuration", - lambda tool=None: pytest.fail("must not prompt when disabled"), - ) - assert self._resolve(None) is None - assert capsys.readouterr().out == "" - - -class TestPolicySummary: - """The box shown to a developer when their admin's config is applied.""" - - MANAGED = { - "default_agent": "claude", - "enabled_agents": {"claude": {"model_config": {"default_model": "system.ai.opus"}}}, - "budget_policy": { - "display_name": "paved-path", - # A fraction of the budget, as the API validates it: 0.8 renders as "at 80%". - "tiers": [ - {"spending_percentage": 0.8, "default_agent": "opencode", "default_model": "haiku"} - ], - }, - } - - def test_lists_the_tiers_and_the_applied_model(self, capsys): - import ucode.cli as cli_mod - - cli_mod._print_managed_summary(self.MANAGED, {"workspace": "https://w"}, "claude") - out = capsys.readouterr().out - assert "paved-path" in out - assert "at 80%" in out and "OpenCode" in out and "haiku" in out - assert "system.ai.opus" in out - - def test_lists_managed_mcps_and_skills(self, capsys): - import ucode.cli as cli_mod - - managed = { - **self.MANAGED, - "mcp_servers": [{"name": "system.ai.slack", "type": "mcp-service"}], - "skills": {"names": ["main.default.my_skill"]}, - } - cli_mod._print_managed_summary(managed, {"workspace": "https://w"}, "claude") - out = capsys.readouterr().out - assert "system.ai.slack" in out - assert "main.default.my_skill" in out - # Marked pending until ucode registers them locally. - assert "pending" in out - - def test_mcp_and_skill_rows_say_none_when_the_config_names_none(self, capsys): - import ucode.cli as cli_mod - - # Shown rather than omitted: a missing row leaves "my admin set none" ambiguous. - cli_mod._print_managed_summary(self.MANAGED, {"workspace": "https://w"}, "claude") - out = capsys.readouterr().out - assert "MCPs:" in out and "Skills:" in out - assert out.count("none configured") == 2 - assert "pending" not in out - - def test_no_policy_rows_without_a_budget_policy(self, capsys): - import ucode.cli as cli_mod - - cli_mod._print_managed_summary( - {"enabled_agents": {"claude": {}}}, {"workspace": "w"}, "claude" - ) - out = capsys.readouterr().out - assert "Policy:" not in out - assert "Claude Code" in out - - class TestBareUcode: """Bare `ucode` launches the managed default agent, or explains why it can't.""" @@ -3963,7 +3321,6 @@ def _run( coding_agent_config_feature_disabled=False, ): launched: list[tuple] = [] - monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1") monkeypatch.setattr("ucode.cli.install_databricks_cli", lambda *a, **k: None) monkeypatch.setattr("ucode.cli.apply_pat_environment", lambda *a, **k: None) monkeypatch.setattr("ucode.cli.load_state", lambda: {"workspace": "https://w"}) @@ -4038,24 +3395,16 @@ def test_non_admin_without_a_config_is_told_to_ask(self, monkeypatch): assert launched == [] assert "Ask a workspace admin" in result.output - def test_admin_without_a_config_sees_no_setup_when_feature_disabled(self, monkeypatch): + def test_feature_disabled_guides_without_managed_mention(self, monkeypatch): result, launched = self._run( monkeypatch, managed=None, is_admin=True, coding_agent_config_feature_disabled=True ) assert result.exit_code == 0, result.output assert launched == [] - assert "ug setup" not in result.output - - def test_non_admin_without_a_config_sees_no_setup_when_feature_disabled(self, monkeypatch): - result, launched = self._run( - monkeypatch, managed=None, is_admin=False, coding_agent_config_feature_disabled=True - ) - assert result.exit_code == 0, result.output - assert launched == [] - assert "ug setup" not in result.output + assert "ug configure" in result.output + assert "managed" not in result.output.lower() def test_dry_run_uses_the_cache_and_does_not_fetch(self, monkeypatch): - monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1") monkeypatch.setattr("ucode.cli.install_databricks_cli", lambda *a, **k: None) monkeypatch.setattr("ucode.cli.apply_pat_environment", lambda *a, **k: None) monkeypatch.setattr("ucode.cli.load_state", lambda: {"workspace": "https://w"}) @@ -4077,7 +3426,6 @@ def test_dry_run_with_no_cached_config_does_not_crash(self, monkeypatch): # --dry-run doesn't fetch, so the feature-disabled flag is never assigned by the fetch path. # With no cached config it must still be well-defined (defaults False) rather than raising # UnboundLocalError when the guidance check reads it. - monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1") monkeypatch.setattr("ucode.cli.install_databricks_cli", lambda *a, **k: None) monkeypatch.setattr("ucode.cli.apply_pat_environment", lambda *a, **k: None) monkeypatch.setattr("ucode.cli.load_state", lambda: {"workspace": "https://w"}) @@ -4100,7 +3448,6 @@ def test_dry_run_with_no_cached_config_does_not_crash(self, monkeypatch): def test_skip_preflight_still_resolves_an_agent_from_the_managed_config(self, monkeypatch): # --skip-preflight is now only about auth/gateway re-validation, decoupled from managed # config, so bare `ucode --skip-preflight` still fetches the config and picks its agent. - monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1") monkeypatch.setattr("ucode.cli.install_databricks_cli", lambda *a, **k: None) monkeypatch.setattr("ucode.cli.apply_pat_environment", lambda *a, **k: None) monkeypatch.setattr("ucode.cli.load_state", lambda: {"workspace": "https://w"}) @@ -4121,35 +3468,8 @@ def test_skip_preflight_still_resolves_an_agent_from_the_managed_config(self, mo assert seen["tool"] == "claude" assert seen["skip_preflight"] is True - def test_skip_managed_config_behaves_as_feature_off(self, monkeypatch): - # --skip-managed-config clears the enabling env var, so bare `ucode` has no config to pick an - # agent from and just prints help — exactly the feature-off behavior, no fetch. - monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1") - monkeypatch.setattr( - "ucode.cli.refresh_managed_config", - lambda state: pytest.fail("--skip-managed-config must not fetch"), - ) - result = runner.invoke(app, ["--skip-managed-config"]) - assert result.exit_code == 0, result.output - assert "Usage:" in result.output - - @pytest.mark.parametrize("env_value", [None, "", "0"]) - def test_prints_help_when_the_env_var_is_off(self, monkeypatch, env_value): - if env_value is None: - monkeypatch.delenv("ENABLE_MANAGED_AGENT_CONFIG", raising=False) - else: - monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", env_value) - monkeypatch.setattr( - "ucode.cli.refresh_managed_config", - lambda state: pytest.fail("must not fetch when disabled"), - ) - result = runner.invoke(app, []) - assert result.exit_code == 0, result.output - assert "Usage:" in result.output - def test_subcommands_still_work(self, monkeypatch): # The callback runs for every invocation, so it must not intercept `ucode status`. - monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1") monkeypatch.setattr( "ucode.cli.refresh_managed_config", lambda state: pytest.fail("the callback must not run for a subcommand"), @@ -4158,29 +3478,6 @@ def test_subcommands_still_work(self, monkeypatch): result = runner.invoke(app, ["status"]) assert result.exit_code == 0, result.output - def test_launcher_skip_managed_config_does_not_fetch(self, monkeypatch): - # `ucode claude --skip-managed-config` clears the env var, so the launch never reads the - # workspace's managed config and falls back to the developer's own settings. - monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1") - monkeypatch.setattr( - "ucode.cli.refresh_managed_config", - lambda state: pytest.fail("--skip-managed-config must not fetch"), - ) - state = dict(MINIMAL_STATE) - with ( - patch("ucode.cli.load_state", return_value=state), - patch("ucode.cli.apply_pat_environment"), - patch("ucode.cli.ensure_bootstrap_dependencies"), - patch("ucode.cli.ensure_provider_state", return_value=state), - patch("ucode.cli.configure_shared_state", return_value=state), - patch("ucode.cli.configure_tool", return_value=state), - patch("ucode.cli.get_databricks_token", return_value="tok"), - patch("ucode.cli.launch_agent"), - ): - result = runner.invoke(app, ["claude", "--skip-managed-config"]) - assert result.exit_code == 0, result.output - assert "managed coding agent config" not in result.output - class TestBudgetRecommendationAtLaunch: """The budget read informs the launch; it never blocks it.""" @@ -4210,13 +3507,11 @@ def fake_recommendation(workspace, token): return result, calls, cfg def test_not_checked_without_a_managed_config(self, monkeypatch): - monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1") result, calls, _ = self._launch(monkeypatch, managed=None) assert result.exit_code == 0, result.output assert calls == [] def test_the_recommended_agent_gets_the_recommended_model(self, monkeypatch): - monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1") managed = { "enabled_agents": { "claude": {"model_config": {"default_model": "system.ai.claude-opus-4-8"}} @@ -4230,7 +3525,6 @@ def test_the_recommended_agent_gets_the_recommended_model(self, monkeypatch): assert cfg.call_args.args[2] == "system.ai.claude-haiku-4-5" def test_passes_configured_claude_defaults_to_writer(self, monkeypatch): - monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1") managed = { "enabled_agents": { "claude": { @@ -4253,7 +3547,6 @@ def test_passes_configured_claude_defaults_to_writer(self, monkeypatch): def test_another_agent_keeps_its_own_model_and_is_told_why(self, monkeypatch): # A tier's model belongs to the tier's agent; pinning it on claude would land a Kimi id in # ANTHROPIC_MODEL, which the Anthropic-dialect endpoint cannot serve. - monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1") managed = { "enabled_agents": { "claude": {"model_config": {"default_model": "system.ai.claude-opus-4-8"}}, @@ -4275,7 +3568,6 @@ def test_another_agent_keeps_its_own_model_and_is_told_why(self, monkeypatch): assert "recommends OpenCode" in result.output def test_a_failed_read_does_not_block_the_launch(self, monkeypatch): - monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1") result, _calls, _cfg = self._launch( monkeypatch, managed={"enabled_agents": {"claude": {}}}, @@ -4287,7 +3579,6 @@ def test_a_failed_read_does_not_block_the_launch(self, monkeypatch): def test_a_token_failure_does_not_block_the_launch(self, monkeypatch): # Auth can lapse between the config refresh and the budget check. - monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1") state = dict(MINIMAL_STATE) monkeypatch.setattr("ucode.cli.get_model_recommendation", lambda ws, tok: (None, None)) with ( @@ -4309,7 +3600,6 @@ def test_a_token_failure_does_not_block_the_launch(self, monkeypatch): assert "Could not check your budget" in result.output def test_shows_the_budget_bar(self, monkeypatch): - monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1") result, _calls, _cfg = self._launch( monkeypatch, managed={"enabled_agents": {"claude": {}}}, @@ -4324,16 +3614,6 @@ def test_shows_the_budget_bar(self, monkeypatch): assert "83% used" in result.output assert "█" in result.output - @pytest.mark.parametrize("env_value", [None, "", "0"]) - def test_not_checked_when_the_env_var_is_off(self, monkeypatch, env_value): - if env_value is None: - monkeypatch.delenv("ENABLE_MANAGED_AGENT_CONFIG", raising=False) - else: - monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", env_value) - result, calls, _ = self._launch(monkeypatch, managed=None) - assert result.exit_code == 0, result.output - assert calls == [] - class TestMcpProxyCmdForwardsUsePat: """`ucode mcp-proxy` forwards the PAT choice to `serve`, which owns the