From 396109ef2e9857cfc85f22bd589113102ea041fb Mon Sep 17 00:00:00 2001 From: Lilly Luo Date: Fri, 21 Aug 2026 12:15:24 +0000 Subject: [PATCH 1/4] fix --- src/ucode/managed_wizard.py | 17 ++++++++++++----- tests/test_managed_wizard.py | 26 ++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/ucode/managed_wizard.py b/src/ucode/managed_wizard.py index 5f7be5d..1e06d8f 100644 --- a/src/ucode/managed_wizard.py +++ b/src/ucode/managed_wizard.py @@ -90,6 +90,11 @@ "codex": "managed_config.toml", } +# Shown whenever the workspace's coding-agent-config APIs return FEATURE_DISABLED. +CODING_AGENT_CONFIGS_DISABLED_MESSAGE = ( + "Coding agent governance is not enabled for this workspace yet." +) + BUDGET_POLICY_BLURB = ( "As the workspace spends more of a budget, a tiered spend policy automatically switches " "everyone's default agent and model to a cheaper one — for example Claude Code / Opus normally, " @@ -1277,6 +1282,12 @@ def _handle_existing_config(workspace: str, token: str) -> tuple[bool, dict | No with spinner("Checking for an existing managed config..."): existing, reason = get_managed_config(workspace, token) if reason is not None: + if "feature_disabled" in reason.lower(): + # The coding-agent-config APIs aren't enabled for this workspace, so there can't be an + # existing config to reconcile. Say so plainly rather than dumping the raw 404 body, + # which reads like a spurious "not found". + print_warning(CODING_AGENT_CONFIGS_DISABLED_MESSAGE) + return True, None print_note(f"Could not check for an existing config: {reason}") return True, None if existing is None: @@ -1928,11 +1939,7 @@ def show_command() -> int: def _explain_publish_failure(reason: str) -> str: lowered = reason.lower() if "feature_disabled" in lowered: - return ( - "Managed coding-agent configs aren't enabled on this workspace yet. Ask your Databricks " - "contact to enable the `codingAgentConfigCrudEnabled` flag for it, then re-run " - "`ucode apply`." - ) + return CODING_AGENT_CONFIGS_DISABLED_MESSAGE if "permission_denied" in lowered or "http 403" in lowered: return ( "Publishing a managed config requires workspace admin. Your account can read the " diff --git a/tests/test_managed_wizard.py b/tests/test_managed_wizard.py index 8cdd9a2..2ff070a 100644 --- a/tests/test_managed_wizard.py +++ b/tests/test_managed_wizard.py @@ -247,6 +247,28 @@ def test_read_failure_continues_with_a_note(self): assert not select.called assert note.called + def test_feature_disabled_continues_with_a_clean_warning(self): + # When the coding-agent-config APIs aren't enabled, the read fails with a FEATURE_DISABLED + # 404. Continue authoring, but warn plainly instead of dumping the raw 404 body. + reason = ( + 'HTTP 404 Not Found: {"error_code":"FEATURE_DISABLED",' + '"message":"Coding agent config APIs are not enabled for this workspace."}' + ) + with ( + patch.object(wizard, "get_managed_config", return_value=(None, reason)), + patch.object(wizard, "prompt_for_selection") as select, + patch.object(wizard, "print_warning") as warn, + patch.object(wizard, "print_note") as note, + ): + assert wizard._handle_existing_config(WORKSPACE, "token") == (True, None) + assert not select.called + message = warn.call_args[0][0] + assert message == wizard.CODING_AGENT_CONFIGS_DISABLED_MESSAGE + # The raw 404 / JSON body must not leak into the message. + assert "404" not in message + assert "FEATURE_DISABLED" not in message + assert not note.called + def test_choosing_create_continues_authoring(self): with ( patch.object( @@ -2527,11 +2549,11 @@ def test_existing_config_without_a_resource_name_is_an_error(self): class TestPublishFailureMessages: """The server's error codes, turned into something an admin can act on.""" - def test_feature_disabled_names_the_flag(self): + def test_feature_disabled_uses_the_shared_message(self): message = wizard._explain_publish_failure( 'HTTP 400 Bad Request: {"error_code":"FEATURE_DISABLED","message":"..."}' ) - assert "codingAgentConfigCrudEnabled" in message + assert message == wizard.CODING_AGENT_CONFIGS_DISABLED_MESSAGE def test_permission_denied_says_admin_is_required(self): message = wizard._explain_publish_failure( From ef61f396a3a99881ca2b6e1c1fdce927cade34db Mon Sep 17 00:00:00 2001 From: Rohit Agrawal Date: Fri, 21 Aug 2026 08:09:30 -0700 Subject: [PATCH 2/4] Clarify managed config unavailable message --- src/ucode/managed_wizard.py | 4 +++- tests/test_managed_wizard.py | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/ucode/managed_wizard.py b/src/ucode/managed_wizard.py index 1e06d8f..5777baf 100644 --- a/src/ucode/managed_wizard.py +++ b/src/ucode/managed_wizard.py @@ -92,7 +92,9 @@ # Shown whenever the workspace's coding-agent-config APIs return FEATURE_DISABLED. CODING_AGENT_CONFIGS_DISABLED_MESSAGE = ( - "Coding agent governance is not enabled for this workspace yet." + "Workspace-managed coding agent configuration is not available on this workspace, so this " + "configuration cannot be published. Use `ucode configure` to set up agents for individual " + "users instead." ) BUDGET_POLICY_BLURB = ( diff --git a/tests/test_managed_wizard.py b/tests/test_managed_wizard.py index 2ff070a..6bddb56 100644 --- a/tests/test_managed_wizard.py +++ b/tests/test_managed_wizard.py @@ -264,6 +264,8 @@ def test_feature_disabled_continues_with_a_clean_warning(self): assert not select.called message = warn.call_args[0][0] assert message == wizard.CODING_AGENT_CONFIGS_DISABLED_MESSAGE + assert "cannot be published" in message + assert "`ucode configure`" in message # The raw 404 / JSON body must not leak into the message. assert "404" not in message assert "FEATURE_DISABLED" not in message @@ -2554,6 +2556,8 @@ def test_feature_disabled_uses_the_shared_message(self): 'HTTP 400 Bad Request: {"error_code":"FEATURE_DISABLED","message":"..."}' ) assert message == wizard.CODING_AGENT_CONFIGS_DISABLED_MESSAGE + assert "cannot be published" in message + assert "`ucode configure`" in message def test_permission_denied_says_admin_is_required(self): message = wizard._explain_publish_failure( From 94fbb313b0bf53c88935dc4d845381fc697e8549 Mon Sep 17 00:00:00 2001 From: Rohit Agrawal Date: Fri, 21 Aug 2026 08:11:43 -0700 Subject: [PATCH 3/4] Stop setup when managed configs are unavailable --- src/ucode/managed_wizard.py | 13 +++++-------- tests/test_managed_wizard.py | 12 +++++------- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/ucode/managed_wizard.py b/src/ucode/managed_wizard.py index 5777baf..a38fca6 100644 --- a/src/ucode/managed_wizard.py +++ b/src/ucode/managed_wizard.py @@ -92,9 +92,8 @@ # Shown whenever the workspace's coding-agent-config APIs return FEATURE_DISABLED. CODING_AGENT_CONFIGS_DISABLED_MESSAGE = ( - "Workspace-managed coding agent configuration is not available on this workspace, so this " - "configuration cannot be published. Use `ucode configure` to set up agents for individual " - "users instead." + "Workspace-managed coding agent configuration is not available on this workspace. Use " + "`ucode configure` to set up agents for individual users instead." ) BUDGET_POLICY_BLURB = ( @@ -1285,11 +1284,9 @@ def _handle_existing_config(workspace: str, token: str) -> tuple[bool, dict | No existing, reason = get_managed_config(workspace, token) if reason is not None: if "feature_disabled" in reason.lower(): - # The coding-agent-config APIs aren't enabled for this workspace, so there can't be an - # existing config to reconcile. Say so plainly rather than dumping the raw 404 body, - # which reads like a spurious "not found". - print_warning(CODING_AGENT_CONFIGS_DISABLED_MESSAGE) - return True, None + # Authoring a draft that the workspace cannot publish only leads the admin through a + # dead-end wizard. Stop before model discovery and point them to per-user setup instead. + raise RuntimeError(CODING_AGENT_CONFIGS_DISABLED_MESSAGE) print_note(f"Could not check for an existing config: {reason}") return True, None if existing is None: diff --git a/tests/test_managed_wizard.py b/tests/test_managed_wizard.py index 6bddb56..bb6d4cd 100644 --- a/tests/test_managed_wizard.py +++ b/tests/test_managed_wizard.py @@ -247,9 +247,9 @@ def test_read_failure_continues_with_a_note(self): assert not select.called assert note.called - def test_feature_disabled_continues_with_a_clean_warning(self): + def test_feature_disabled_blocks_setup_with_an_actionable_error(self): # When the coding-agent-config APIs aren't enabled, the read fails with a FEATURE_DISABLED - # 404. Continue authoring, but warn plainly instead of dumping the raw 404 body. + # 404. Stop before authoring a draft that can never be published. reason = ( 'HTTP 404 Not Found: {"error_code":"FEATURE_DISABLED",' '"message":"Coding agent config APIs are not enabled for this workspace."}' @@ -257,14 +257,13 @@ def test_feature_disabled_continues_with_a_clean_warning(self): with ( patch.object(wizard, "get_managed_config", return_value=(None, reason)), patch.object(wizard, "prompt_for_selection") as select, - patch.object(wizard, "print_warning") as warn, patch.object(wizard, "print_note") as note, + pytest.raises(RuntimeError) as exc_info, ): - assert wizard._handle_existing_config(WORKSPACE, "token") == (True, None) + wizard._handle_existing_config(WORKSPACE, "token") assert not select.called - message = warn.call_args[0][0] + message = str(exc_info.value) assert message == wizard.CODING_AGENT_CONFIGS_DISABLED_MESSAGE - assert "cannot be published" in message assert "`ucode configure`" in message # The raw 404 / JSON body must not leak into the message. assert "404" not in message @@ -2556,7 +2555,6 @@ def test_feature_disabled_uses_the_shared_message(self): 'HTTP 400 Bad Request: {"error_code":"FEATURE_DISABLED","message":"..."}' ) assert message == wizard.CODING_AGENT_CONFIGS_DISABLED_MESSAGE - assert "cannot be published" in message assert "`ucode configure`" in message def test_permission_denied_says_admin_is_required(self): From b93c29607aa7402021af1ba1bc0cc85b31159b00 Mon Sep 17 00:00:00 2001 From: Rohit Agrawal Date: Fri, 21 Aug 2026 08:13:09 -0700 Subject: [PATCH 4/4] Use shared error for apply preflight --- src/ucode/managed_wizard.py | 2 ++ tests/test_managed_wizard.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/ucode/managed_wizard.py b/src/ucode/managed_wizard.py index a38fca6..dab1882 100644 --- a/src/ucode/managed_wizard.py +++ b/src/ucode/managed_wizard.py @@ -2037,6 +2037,8 @@ def apply_command(*, yes: bool = False) -> int: with spinner("Checking for an existing managed config..."): existing, reason = get_managed_config(workspace, token) if reason is not None: + if "feature_disabled" in reason.lower(): + raise RuntimeError(CODING_AGENT_CONFIGS_DISABLED_MESSAGE) raise RuntimeError( f"Could not check whether {workspace} already has a managed config: {reason}. " "Refusing to publish without knowing, since that could overwrite a config silently." diff --git a/tests/test_managed_wizard.py b/tests/test_managed_wizard.py index bb6d4cd..550a67d 100644 --- a/tests/test_managed_wizard.py +++ b/tests/test_managed_wizard.py @@ -2541,6 +2541,24 @@ def fake_create(*a, **k): ) assert created["called"] is False + def test_feature_disabled_read_uses_the_shared_blocking_message(self): + managed_config_mod.save_managed_state(WORKSPACE, self.MANIFEST) + created = {"called": False} + + def fake_create(*a, **k): + created["called"] = True + return {}, None + + reason = 'HTTP 404 Not Found: {"error_code":"FEATURE_DISABLED"}' + with pytest.raises(RuntimeError) as exc_info: + self._run( + get_managed_config=lambda *a, **k: (None, reason), + create_coding_agent_config=fake_create, + ) + assert str(exc_info.value) == wizard.CODING_AGENT_CONFIGS_DISABLED_MESSAGE + assert "FEATURE_DISABLED" not in str(exc_info.value) + assert created["called"] is False + def test_existing_config_without_a_resource_name_is_an_error(self): managed_config_mod.save_managed_state(WORKSPACE, self.MANIFEST) with pytest.raises(RuntimeError, match="resource name"):