diff --git a/README.md b/README.md index f03cad3..37490a2 100644 --- a/README.md +++ b/README.md @@ -186,21 +186,14 @@ skipped. It then offers tracing, managed MCP servers, skills, and a spend-based switches the default agent and model as the workspace burns through a budget. The result is written to `~/.ucode/managed-state.json` — the one local managed-config file — which -`ucode apply` publishes to the workspace. Because a launch reads that same file, `ucode --dry-run` -tries the authored config on this machine before it is published, without fetching or overwriting -it. Your own agent configs are left alone, with one exception: answering yes to tracing, MCP -servers, or skills runs the matching `ucode configure` step, which does configure this machine. +`ucode apply` publishes to the workspace. Your own agent configs are left alone, with one exception: +answering yes to tracing, MCP servers, or skills runs the matching `ucode configure` step, which +does configure this machine. ```bash # Review the manifest and the exact payload `ucode apply` would publish. ucode setup show -# Walk the flow without writing anything. -ucode setup --dry-run - -# Try the authored config locally before publishing (no fetch, no overwrite). -ucode --dry-run - # Skip the prompts and load a hand-written config instead (validated before saving). ucode setup --from-file ./managed-config.json ``` diff --git a/src/ucode/cli.py b/src/ucode/cli.py index 6d2c7ed..a78c68a 100644 --- a/src/ucode/cli.py +++ b/src/ucode/cli.py @@ -33,7 +33,7 @@ ) from ucode.agents.codex import revert_legacy_shared_config from ucode.agents.pi import PI_SETTINGS_BACKUP_PATH, PI_SETTINGS_PATH -from ucode.config_io import is_dry_run, restore_file, set_dry_run +from ucode.config_io import restore_file, set_dry_run from ucode.databricks import ( apply_pat_environment, build_shared_base_urls, @@ -1387,9 +1387,7 @@ def _fetch_budget_recommendation( Enforcement is server-side, so a failed read only costs the recommendation: the config's own ``default_model`` still applies and the launch proceeds. """ - # --dry-run resolves the agent from the last saved config alone, so it must not reach the - # control plane — mirroring the managed-config read, which is likewise skipped under --dry-run. - if managed is None or skip_preflight or is_dry_run(): + if managed is None or skip_preflight: return None reason: str | None = None recommendation = None @@ -1735,8 +1733,8 @@ def _launch_tool( # 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. Skipped under --skip-preflight (deliberately - # unmanaged) and --dry-run (writes nothing). - if managed is not None and not skip_preflight and not is_dry_run(): + # unmanaged). + if managed is not None and not skip_preflight: _register_managed_mcp_servers(managed, tool, state) _apply_managed_skills(managed, tool, state) print_success(f"Starting {TOOL_SPECS[tool]['display']}") @@ -1788,14 +1786,6 @@ def default( is_eager=True, ), ] = False, - dry_run: Annotated[ - bool, - typer.Option( - "--dry-run", - help="Print config files without writing them. Uses the last saved managed " - "config instead of fetching a fresh one.", - ), - ] = False, skip_preflight: SkipPreflightOption = False, workspace: WorkspaceOption = None, ) -> None: @@ -1805,11 +1795,8 @@ def default( """ if ctx.invoked_subcommand is not None: return - set_dry_run(dry_run) try: - _launch_managed_default( - ctx, dry_run=dry_run, skip_preflight=skip_preflight, workspace=workspace - ) + _launch_managed_default(ctx, skip_preflight=skip_preflight, workspace=workspace) except typer.Exit: # `typer.Exit` subclasses RuntimeError, so it has to be re-raised ahead of the handler # below. Otherwise a launch that already reported its own error is followed by @@ -1823,7 +1810,6 @@ def default( def _launch_managed_default( ctx: typer.Context, *, - dry_run: bool, skip_preflight: bool, workspace: str | None, ) -> None: @@ -1845,22 +1831,9 @@ def _launch_managed_default( "--skip-preflight launches with your own settings, so `ucode` has no managed config " "to pick an agent from. Run `ucode --skip-preflight` instead." ) - # --dry-run avoids the fetch but still applies the last saved config. - if dry_run: - managed = load_managed_state(current) - else: - with spinner("Checking for a managed coding agent config..."): - managed = refresh_managed_config(state) + with spinner("Checking for a managed coding agent config..."): + managed = refresh_managed_config(state) if not managed: - # Only a read that actually reached the workspace can say it publishes no config. Under - # --dry-run nothing was fetched, so an empty cache means "not pulled yet" — reporting that - # as "no config" would tell an admin their own published config doesn't exist. - if dry_run: - print_warning( - "No managed coding agent config is saved locally yet, so there is nothing to " - "dry-run. Run `ucode` without --dry-run to pull your workspace's config first." - ) - return _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 @@ -2506,15 +2479,10 @@ def setup( "ucode's manifest shape) instead. Validated before it is saved.", ), ] = None, - dry_run: Annotated[ - bool, - typer.Option("--dry-run", help="Walk the flow without writing any files."), - ] = False, ) -> None: """Author the managed coding config for your workspace (workspace admins only).""" if ctx.invoked_subcommand is not None: return - set_dry_run(dry_run) # `typer.Exit` subclasses RuntimeError, so it must be raised outside the try — inside, the # `except RuntimeError` below would swallow it and report the exit code as an error message. try: diff --git a/src/ucode/managed_config.py b/src/ucode/managed_config.py index 5e9bbce..d7da5bd 100644 --- a/src/ucode/managed_config.py +++ b/src/ucode/managed_config.py @@ -13,8 +13,8 @@ There is deliberately one file, not a separate authored ``managed-settings.json``: the workspace is the source of truth, so an authored draft and the pulled copy are the same shape and coexist in -``managed-state.json``. ``ucode --dry-run`` reads the local draft without fetching or overwriting it, -which is how an admin tries a config out between ``ucode setup`` and ``ucode apply``. +``managed-state.json``. ``ucode setup`` authors the draft; ``ucode apply`` publishes it; a launch +then pulls the published copy back into the same file. :func:`refresh_managed_config` is the launch path's entry point. It is called before model discovery, because the manifest decides whether that discovery is needed at all; the launch path then hands the @@ -379,10 +379,10 @@ def load_managed_state(workspace: str | None) -> dict | None: Returns the normalized config dict (the ``config`` field), only when the stored file is for the same workspace — so a stale file from another workspace is ignored rather than misapplied. - This is the single local managed config: ``ucode setup`` authors it here, ``ucode --dry-run`` - reads it to try the authored config locally, ``ucode apply`` publishes it, and a normal launch - refreshes it from the workspace. The admin-authored draft and the pulled copy share one file - because the workspace is the source of truth — to keep a draft, publish it with ``ucode apply``. + This is the single local managed config: ``ucode setup`` authors it here, ``ucode apply`` + publishes it, and a launch refreshes it from the workspace. The admin-authored draft and the + pulled copy share one file because the workspace is the source of truth — to keep a draft, + publish it with ``ucode apply``. """ if not workspace: return None diff --git a/src/ucode/managed_wizard.py b/src/ucode/managed_wizard.py index f54c2db..e175550 100644 --- a/src/ucode/managed_wizard.py +++ b/src/ucode/managed_wizard.py @@ -3,9 +3,8 @@ Workspace admins run this to build the ``CodingAgentConfig`` their developers will pull. It walks the admin through agents, per-agent models, tracing, MCP servers, skills, and a spend-routing budget policy, then writes the manifest to ``~/.ucode/managed-state.json`` (the one local managed-config -file, owned by :mod:`ucode.managed_config`). An admin can try it with ``ucode --dry-run`` and then -publish it to the workspace with ``ucode apply`` (a separate command, so the file can be reviewed -first). +file, owned by :mod:`ucode.managed_config`). Publishing it to the workspace is ``ucode apply`` (a +separate command, so an admin can review the file first). Serialization, validation, and the per-agent model catalogs live in :mod:`ucode.managed_setup`; this module is the interaction layer on top of them. Sub-flows an admin already knows — tracing, MCP, @@ -20,7 +19,6 @@ from typing import cast from ucode.agents import TOOL_SPECS, check_gateway_endpoint -from ucode.config_io import is_dry_run from ucode.databricks import ( ANTHROPIC_FAMILIES, all_users_can_use_schema, @@ -936,7 +934,7 @@ def _delete_existing_config(workspace: str, token: str, existing: dict) -> None: """Delete the workspace's published config after confirming. Raises RuntimeError on failure. Deleting leaves the workspace with no managed config, so every developer falls back to their own - settings on their next ucode run — confirm before doing it, and honor ``--dry-run``. + settings on their next ucode run — confirm before doing it. """ name = existing.get("name") if not isinstance(name, str): @@ -951,9 +949,6 @@ def _delete_existing_config(workspace: str, token: str, existing: dict) -> None: if not prompt_yes_no_default("Delete the existing managed config?", default=False): print_note("Nothing was deleted.") return - if is_dry_run(): - print_success("Dry run: the config was not deleted.") - return with spinner("Deleting the managed config..."): delete_reason = delete_coding_agent_config(workspace, token, name) if delete_reason is not None: @@ -998,23 +993,14 @@ def setup_from_file(path: str) -> int: save_managed_state(workspace, manifest) _render_summary(workspace, manifest) - if is_dry_run(): - print_success( - f"Dry run: {manifest_path.name} was not written to ~/.ucode/managed-state.json." - ) - else: - print_success(f"Saved to {manifest_path.name} -> ~/.ucode/managed-state.json") - _print_next_steps() + print_success(f"Saved to {manifest_path.name} -> ~/.ucode/managed-state.json") + _print_next_steps() return 0 def _print_next_steps() -> None: console.print() print_heading("Next steps") - # The authored manifest is saved to the same local file a launch reads, so `ucode --dry-run` - # previews this machine's agents *as configured by the manifest* without fetching or overwriting - # it — a real local test of the config before it is published. - print_note("Try it locally: ucode --dry-run") print_note("Publish it to the workspace: ucode apply") @@ -1056,8 +1042,7 @@ def setup_command( if not _handle_existing_config(workspace, token): return 0 - # Discover the workspace's models and gateway URLs. This also logs in and persists local state, - # which is what lets the admin dry-run the config on their own machine afterwards. + # Discover the workspace's models and gateway URLs. This also logs in and persists local state. state = configure_shared_state(workspace, profile=profile, force_login=False) workspace = state.get("workspace") or workspace profile = state.get("profile") or profile @@ -1159,11 +1144,8 @@ def setup_command( save_managed_state(workspace, manifest) _render_summary(workspace, manifest) console.print() - if is_dry_run(): - print_success("Dry run: nothing was written to ~/.ucode/managed-state.json.") - else: - print_success("Saved to ~/.ucode/managed-state.json") - _print_next_steps() + print_success("Saved to ~/.ucode/managed-state.json") + _print_next_steps() return 0 diff --git a/tests/test_cli.py b/tests/test_cli.py index cb40e03..181e935 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -2584,25 +2584,6 @@ 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_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"}) - monkeypatch.setattr( - "ucode.cli.refresh_managed_config", - lambda state: pytest.fail("--dry-run must not fetch"), - ) - monkeypatch.setattr("ucode.cli.load_managed_state", lambda ws: self.MANAGED) - launched: list[tuple] = [] - monkeypatch.setattr( - "ucode.cli._launch_tool", lambda tool, ctx, **kw: launched.append((tool, kw)) - ) - result = runner.invoke(app, ["--dry-run"]) - assert result.exit_code == 0, result.output - # The config bare `ucode` already read is handed down, so the launch path does not refetch. - assert launched[0][1]["managed"] == self.MANAGED - def test_skip_preflight_has_no_config_to_pick_an_agent_from(self, monkeypatch): # --skip-preflight is deliberately unmanaged, so bare `ucode` cannot resolve an agent. It # must say that rather than report "no config found", which would be wrong. diff --git a/tests/test_managed_wizard.py b/tests/test_managed_wizard.py index bbe8353..c165f5f 100644 --- a/tests/test_managed_wizard.py +++ b/tests/test_managed_wizard.py @@ -279,7 +279,6 @@ def test_choosing_delete_stops_and_deletes(self): ), patch.object(wizard, "prompt_for_selection", return_value="delete"), patch.object(wizard, "prompt_yes_no_default", return_value=True), - patch.object(wizard, "is_dry_run", return_value=False), patch.object(wizard, "delete_coding_agent_config", return_value=None) as delete, ): assert wizard._handle_existing_config(WORKSPACE, "token") is False @@ -300,21 +299,6 @@ def test_delete_declined_leaves_config_intact(self): assert wizard._handle_existing_config(WORKSPACE, "token") is False assert not delete.called - def test_delete_honors_dry_run(self): - with ( - patch.object( - wizard, - "get_managed_config", - return_value=({"name": "cfg/1", "enabled_agents": {}}, None), - ), - patch.object(wizard, "prompt_for_selection", return_value="delete"), - patch.object(wizard, "prompt_yes_no_default", return_value=True), - patch.object(wizard, "is_dry_run", return_value=True), - patch.object(wizard, "delete_coding_agent_config") as delete, - ): - assert wizard._handle_existing_config(WORKSPACE, "token") is False - assert not delete.called - def test_delete_failure_raises(self): with ( patch.object( @@ -324,7 +308,6 @@ def test_delete_failure_raises(self): ), patch.object(wizard, "prompt_for_selection", return_value="delete"), patch.object(wizard, "prompt_yes_no_default", return_value=True), - patch.object(wizard, "is_dry_run", return_value=False), patch.object(wizard, "delete_coding_agent_config", return_value="HTTP 500"), pytest.raises(RuntimeError, match="Could not delete"), ): @@ -2146,15 +2129,6 @@ def test_from_file_is_forwarded(self): runner.invoke(app, ["setup", "--from-file", "/tmp/x.json"]) assert setup.call_args.kwargs["from_file"] == "/tmp/x.json" - def test_dry_run_sets_the_flag(self): - with ( - patch("ucode.cli.install_databricks_cli"), - patch("ucode.cli.setup_command", return_value=0), - patch("ucode.cli.set_dry_run") as set_flag, - ): - runner.invoke(app, ["setup", "--dry-run"]) - set_flag.assert_called_once_with(True) - def test_show_exits_zero(self): with patch("ucode.cli.show_command", return_value=0): result = runner.invoke(app, ["setup", "show"])