Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
0d8afa6
[AIGTWY-4877] Add MPS and MCP two-agent CUJ
masonc08 Oct 5, 2026
7bb19cd
[AIGTWY-4877] Use automatic CUJ discovery
masonc08 Oct 6, 2026
4e04796
[AIGTWY-4877] Reuse dedicated CUJ framework
masonc08 Oct 6, 2026
1ef1b16
[AIGTWY-4877] Preserve CUJ recorder origin for MPS lookup
masonc08 Oct 6, 2026
518fabb
[AIGTWY-4877] Isolate CUJ2 cases
masonc08 Oct 6, 2026
b4e5a5d
[AIGTWY-4877] Fix MCP auth and Claude permission handling
masonc08 Oct 6, 2026
01ee822
[AIGTWY-4877] Capture Codex MCP startup failures
masonc08 Oct 6, 2026
916e27a
[AIGTWY-4877] Handle CUJ agent startup prompts
masonc08 Oct 6, 2026
3dff52e
[AIGTWY-4877] Defer unsupported sandbox execution
masonc08 Oct 6, 2026
aecb84a
[AIGTWY-4877] Fix Claude model selection in CUJs
masonc08 Oct 6, 2026
92dad14
[AIGTWY-4877] Override saved Claude model for explicit launches
masonc08 Oct 6, 2026
d0ca0ff
[AIGTWY-4877] Make explicit Claude catalog model override saved model
masonc08 Oct 6, 2026
d04b995
[AIGTWY-4877] Fix Claude model pinning and skill confirmation
masonc08 Oct 6, 2026
e920a2a
[AIGTWY-4877] Reuse CUJ request recorder helpers
masonc08 Oct 6, 2026
71f681f
[AIGTWY-4877] Revert CUJ machine settings between cases
masonc08 Oct 6, 2026
1979361
[AIGTWY-4877] Preserve recorder origin for Claude discovery
masonc08 Oct 6, 2026
934904c
[AIGTWY-4877] Trim stale CUJ changes
masonc08 Oct 6, 2026
7ba473a
[AIGTWY-4877] Accept formatted Smart Router confirmation
masonc08 Oct 6, 2026
f187532
[AIGTWY-4877] Clarify CUJ expected config helper
masonc08 Oct 6, 2026
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
11 changes: 10 additions & 1 deletion src/ucode/agents/codex.py
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,11 @@ def _reconcile_managed_config(state: dict, compose: Callable[[dict], dict]) -> N


MANAGED_MCP_CONFIG_KEY = "mcp_servers"
_CODEX_MCP_AUTH_ENV_VARS = [
"DATABRICKS_BEARER",
"DATABRICKS_BEARER_COMMAND",
"DATABRICKS_CONFIG_FILE",
]


def managed_mcp_uses_managed_file() -> bool:
Expand All @@ -627,7 +632,11 @@ def managed_mcp_uses_managed_file() -> bool:

def managed_mcp_entry(argv: list[str]) -> dict:
"""A ``[mcp_servers.<name>]`` stdio entry from the ``ug mcp-proxy`` argv (same as user scope)."""
return {"command": argv[0], "args": list(argv[1:])}
return {
"command": argv[0],
"args": list(argv[1:]),
"env_vars": list(_CODEX_MCP_AUTH_ENV_VARS),
}


def managed_mcp_http_entry(url: str, client_id: str) -> dict:
Expand Down
14 changes: 6 additions & 8 deletions src/ucode/databricks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2384,7 +2384,7 @@ def list_model_provider_services(
# reach the next.
return [dict(service) for service in cached], None

hostname = workspace_hostname(workspace)
origin = normalize_workspace_url(workspace)
services: list[dict] = []
page_token: str | None = None
seen_tokens: set[str] = set()
Expand All @@ -2395,9 +2395,7 @@ def list_model_provider_services(
params["parent"] = f"schemas/{parent}"
if page_token:
params["page_token"] = page_token
url = (
f"https://{hostname}/api/2.1/unity-catalog/model-provider-services?{urlencode(params)}"
)
url = f"{origin}/api/2.1/unity-catalog/model-provider-services?{urlencode(params)}"
payload, reason = _http_get_json(url, token, timeout=30)
if payload is None:
# Surface the failure only if we have nothing yet; a mid-pagination blip still
Expand Down Expand Up @@ -2470,8 +2468,8 @@ def get_model_provider_service(
server-side filter) makes a service that plainly exists look absent. Addressing it directly
removes that whole class of false negative.
"""
hostname = workspace_hostname(workspace)
url = f"https://{hostname}/api/2.1/unity-catalog/model-provider-services/{service_name}"
origin = normalize_workspace_url(workspace)
url = f"{origin}/api/2.1/unity-catalog/model-provider-services/{service_name}"
payload, reason = _http_get_json(url, token, timeout=30)
if payload is None:
return None, reason
Expand Down Expand Up @@ -2956,14 +2954,14 @@ def _get_anthropic_models_json(
parent_schema: str | None = None,
provider: str | None = None,
) -> tuple[dict | list | None, str | None]:
hostname = workspace_hostname(workspace)
origin = normalize_workspace_url(workspace)
headers = None
if provider is not None:
headers = {MODEL_PROVIDER_SERVICE_HEADER: provider}
elif parent_schema is not None:
headers = {MODEL_SERVICE_PARENT_SCHEMA_HEADER: parent_schema}
return _http_get_json(
f"https://{hostname}{ANTHROPIC_MODELS_PATH}?limit=1000",
f"{origin}{ANTHROPIC_MODELS_PATH}?limit=1000",
token,
max_retries=_ANTHROPIC_MODEL_DISCOVERY_SETUP_MAX_RETRIES,
**({"headers": headers} if headers is not None else {}),
Expand Down
25 changes: 23 additions & 2 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,13 @@ These are **implemented assertions**, not a claim that every version passes.
Consult the run's JUnit report and artifacts for results. Each function states
its **Scenario** and **Expected** outcome and shows its configure and launch
commands. Fixtures supply fresh environments and credentials, never configured ug.
All tests live directly in `integration/`; shared mechanics live in `utils/`.
Integration tests live directly in `integration/`; shared mechanics live in `utils/`.
Dedicated-workspace CUJs live in `e2e_cuj/` and use the shared `cuj` fixture plus
integration utilities; only CUJ-specific evidence correlation stays in a test file.

| Test | User action | Expected evidence |
| --- | --- | --- |
| `test_cuj_configuration`, `test_cuj_codex_inference`, `test_cuj_claude_inference` in `test_cuj2_mps_mcp.py` | Read the permanently preconfigured two-agent MPS/MCP config; run separate configure, Codex TUI, and Claude TUI cases | Configuration verifies both native MPS APIs, generated settings, and sandbox inclusion with web_search excluded; each TUI case records its exact MPS target header and model, paired HTTP 200 response, and final marker. Live sandbox execution is deferred because MAS cannot downscope the CI service principal. No workspace config CRUD is performed |
| `test_ug_configure_claude_databricks` | Configure Databricks Hosted; execute the generated auth helper; launch plain `ug claude`, read a file, and open `/model` | Generated helper invokes `ug` with clean token stdout; assistant returns an unpredictable file value; native discovery caches `system.ai` models and the picker shows a discovered model without an opt-in flag; normal exit; reopen with working keyboard input |
| `test_ug_configure_claude_anthropic_mps` | Select Anthropic MPS in the real configure picker; launch Claude | Saved provider in status; completed TUI file task; normal exit |
| `test_ug_configure_codex_databricks` | Configure Databricks Hosted; execute the generated auth helper; open Codex TUI and read a file | Generated helper invokes `ug` with clean token stdout; completed assistant answer contains the file value; normal exit and reopen |
Expand Down Expand Up @@ -209,6 +212,23 @@ deduplication, and raw catalog ID/display-name rows for scoped pickers.
Managed discovery expectations come from separate read-only, provider-scoped
model-list requests; they do not rely solely on ug's generated catalog.

The dedicated-workspace CUJ suite adds three independently runnable CUJ2 MPS+MCP cases:
one configuration case and separate Codex and Claude TUI task cases. The workspace is
preconfigured for CUJ2 and is read-only; every case validates it, and the fixture compares
it again during teardown. The TUI cases forward agent requests through the shared per-test
loopback recorder to the real workspace and assert the provider-service header, model,
paired HTTP 200 response, and final answer. The configuration case verifies sandbox MCP
selection and generated client listings; live sandbox execution remains deferred because
MAS cannot downscope the CI service principal. Failed TUI runs
retain terminal output/actions/screen and best-effort agent transcript snapshots under the
runner artifact directory before the temporary homes are deleted. The suite requires
`UG_CUJ_SP_CLIENT_ID` and `UG_CUJ_SP_CLIENT_SECRET`, with `ug`, `claude`, `codex`, and
`databricks` on PATH; run it with
`uv run --with pexpect==4.9.0 --with pyte==0.8.2 pytest --confcutdir=tests/e2e_cuj tests/e2e_cuj`.
Pytest prints the per-test artifact directory; the fixture forwards only its short-lived
bearer to the isolated session. It reads the published config as an input only and does not
use `UCODE_MANAGED_CONFIG_STUB`.

ug no longer runs a post-configure agent probe; the deprecated `--skip-validate`
flag is accepted as a no-op where older journeys still pass it. Tests retain
`--skip-upgrade` as a deprecated no-op too; UG only upgrades agents below its
Expand Down Expand Up @@ -288,7 +308,8 @@ These are unit/component checks; they do not establish live sudo password-prompt

| Scenario | Status / requirement |
| --- | --- |
| Live MCP and skills functionality | Deferred; installation tests cover the local web-search MCP handshake and tool listing, not upstream proxying or a real search request. Custom OAuth search dispatch/refresh has component coverage; live parent/child search and permission decisions remain unverified |
| Broad live MCP functionality | CUJ2 covers sandbox MCP configuration and generated client listings. Live sandbox execution is deferred because MAS cannot downscope the CI service principal. Installation tests cover the local web-search MCP handshake and tool listing, not upstream proxying or a real search request. Other MCP services, live parent/child search, and permission decisions remain deferred |
| Dedicated CUJ Skill coverage | Deferred because of a backend storage-path issue; CUJ2 has no Skill configuration, download, invocation, or assertion path |
| Broad configure flags, multiple workspaces, and PAT flows | Deferred while focusing on basic CUJs |
| Workspace-switch MCP cleanup | The `workspace_switch` CUJ covers real registration, cleanup, repeat configure, and a completed Claude task. Unit/component tests cover duplicate attempts and injected removal failures; the CUJ does not force an agent timeout. It runs in the required managed CI lane for full/live runs. |
| Relayed/subscription MPS discovery | Not covered by the scoped discovery journeys |
Expand Down
14 changes: 10 additions & 4 deletions tests/e2e_cuj/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
Each CUJ owns a separate workspace. Subclass `BaseCujTest` from `base.py` and set
`WORKSPACE_URL`. Setup provides `self.workspace`, a Databricks SDK client using
`UG_CUJ_SP_CLIENT_ID` and `UG_CUJ_SP_CLIENT_SECRET` with OAuth M2M authentication.
The workspaces share one metastore, so metastore-scoped fixture names belong in
`helpers/constants.py`; keep each workspace URL in its test class.
All workspace configurations are READ ONLY. Never create, publish, update, delete,
or restore workspace configuration, including during setup or teardown. Validate
the preconfigured workspace and fail on a mismatch; never repair it. Concurrent
Expand Down Expand Up @@ -44,10 +46,14 @@ In `helpers/evidence.py`, `ClaudeCujHelper` and `CodexCujHelper` implement
Model evidence combines observed gateway requests/responses with native completed-turn
records; neither routing banners nor native records alone prove an applied decision.

`test_cuj_smart_routing.py` runs four fresh interactive sessions: routed and explicit
model, for both Claude and Codex. It uses the published models/defaults and asserts
configuration is unchanged, including during teardown after failures. Routing-disabled
coverage requires a separately preconfigured workspace and is not part of this CUJ.
`test_cuj2_mps_mcp.py` runs three independently collected cases against a permanently
preconfigured workspace: configuration, Codex inference, and Claude inference. Each case
reads and validates the exact two-agent MPS/MCP shape. No case creates, updates, deletes,
or restores CodingAgentConfig. The fixture's final read-only equality check is the only
workspace mutation guard. The test uses the shared `UserSession`, `Workspace`, and
`TuiRequestRecorder`, `AgentTerminal`, and integration transcript readers directly. The
configuration case verifies the selected sandbox service and generated MCP listings; live
`run_code` execution remains deferred because MAS cannot downscope the CI service principal.

Use a clean disposable POSIX runner without existing machine-wide agent settings.
Install the intended versions of `ug`, `claude`, `codex`, and `databricks` on PATH.
Expand Down
21 changes: 20 additions & 1 deletion tests/e2e_cuj/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import pytest
from databricks.sdk.errors import DatabricksError

from tests.integration.utils.terminal import TerminalProcess

from .helpers.constants import CLAUDE, CODEX, MANAGED_PATHS
from .helpers.session import UserSession
from .helpers.tui_request_recorder import TuiRequestRecorder
Expand Down Expand Up @@ -45,7 +47,24 @@ def cuj(request, setup_workspace, tmp_path_factory):
try:
yield session, workspace, recorder
finally:
workspace.assert_unchanged(published)
try:
workspace.assert_unchanged(published)
finally:
state_dir = session.home / ".ucode"
if any(
(state_dir / name).is_file()
for name in ("state.json", "managed-backups/manifest.json")
):
with TerminalProcess(
session,
"ug",
[str(session.binary), "revert"],
"cleanup-revert",
) as terminal:
terminal.finish()
assert not any(path.exists() for path in MANAGED_PATHS), (
"CUJ teardown left machine-wide agent settings"
)
except DatabricksError as error:
# Server messages may echo credentials; retain only the SDK error type.
raise RuntimeError(f"Workspace API failed: {type(error).__name__}") from None
16 changes: 15 additions & 1 deletion tests/e2e_cuj/helpers/constants.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""CLI agent names and public API wire values shared by CUJs."""
"""Agent, metastore fixture, and transcript values shared by CUJs."""

from enum import StrEnum
from pathlib import Path
Expand All @@ -20,3 +20,17 @@
class CodingAgent(StrEnum):
CLAUDE_CODE = "CODING_AGENT_CLAUDE_CODE"
CODEX = "CODING_AGENT_CODEX"


CODING_AGENT_BY_CLI_NAME = {
CLAUDE: CodingAgent.CLAUDE_CODE,
CODEX: CodingAgent.CODEX,
}

MODEL_PROVIDER_SERVICE_FIXTURES = {
CLAUDE: ("ug_e2e.providers.anthropic", "claude-haiku-4-5-20251001"),
CODEX: ("ug_e2e.providers.openai", "gpt-5-nano"),
}

SANDBOX_MCP_SERVICE_NAME = "system.ai.sandbox"
WEB_SEARCH_MCP_SERVICE_NAME = "system.ai.web_search"
Loading
Loading