Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 47 additions & 2 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -428,11 +428,56 @@ jobs:
${{ runner.temp }}/ug-integration/artifacts/
${{ runner.temp }}/ug-integration/wheels/

cuj1:
name: CUJ1 managed config sentinel
if: ${{ inputs.suite != 'installation' && inputs.suite != 'smoke' && inputs.suite != 'tui' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
runs-on: ubuntu-22.04
timeout-minutes: 70
concurrency:
group: e2e-cuj1
cancel-in-progress: false
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22.19.0
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
version: 0.9.8
- uses: databricks/setup-cli@bdb89f81c11a5bd647fd55b585b7c396ec68a25a # v1.0.0
with:
version: 1.17.0
- name: Run CUJ1 against its dedicated workspace
shell: bash
env:
UV_DEFAULT_INDEX: ${{ env.PACKAGE_INDEX }}
UG_CUJ1_WORKSPACE: ${{ secrets.UG_CUJ1_WORKSPACE }}
UG_CUJ_SP_CLIENT_ID: ${{ secrets.UG_CUJ_SP_CLIENT_ID }}
UG_CUJ_SP_CLIENT_SECRET: ${{ secrets.UG_CUJ_SP_CLIENT_SECRET }}
run: |
test -n "$UG_CUJ1_WORKSPACE"
npm install --global --no-audit --no-fund \
"@anthropic-ai/claude-code@$CLAUDE_VERSION" "@openai/codex@$CODEX_VERSION"
mkdir -p "$RUNNER_TEMP/ug-cuj1"
uv lock
uv run --frozen --python 3.12 pytest --confcutdir=tests/e2e_cuj \
tests/e2e_cuj --junitxml="$RUNNER_TEMP/ug-cuj1/junit.xml"
- name: Upload CUJ1 evidence
if: ${{ !cancelled() }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: integration-cuj1
include-hidden-files: true
path: |
${{ runner.temp }}/ug-cuj1/*.xml

cujs:
name: All integration tests
if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
# installation-windows remains advisory until its initial platform issues are fixed.
needs: [installation, workspace, smoke, full, managed]
needs: [installation, workspace, smoke, full, managed, cuj1]
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
Expand All @@ -454,7 +499,7 @@ jobs:
if suite != "smoke":
required.append("full")
if suite in {"full", "live"}:
required.append("managed")
required.extend(["managed", "cuj1"])
failed = [job for job in required if results[job]["result"] != "success"]
if failed:
raise SystemExit("Integration jobs did not pass: " + ", ".join(failed))
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ fallback-version = "0.0.0"
[dependency-groups]
dev = [
"pytest>=9.0.3",
"pexpect>=4.9.0",
"pyte>=0.8.2",
# Pinned so local and CI lint with the identical formatter/type-checker;
# ruff/ty output shifts between releases, so bump these deliberately.
"ruff==0.16.5",
Expand Down
1 change: 1 addition & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ mocks, monkeypatching, fake binaries/services, or fabricated ug state.
| Unit/component | Existing `test_*.py` files | Individual behavior; dependencies may be mocked |
| Existing e2e | `test_e2e*.py` | Real workspace behavior with some patched setup/internal calls |
| Integration CUJs | `integration/test_*.py` | Public configure, TUI, script, command, protocol, and lifecycle journeys |
| Dedicated-workspace CUJs | `e2e_cuj/test_*.py` | Published workspace policy, real agent tasks, and observable agent traces |
| Installation | `integration/test_installation.py` | Fresh installed package, CLI, and local helpers without credentials on Linux and advisory native Windows |

`test_entry_points.py` also runs both installed console scripts (`ug` and `ucode`)
Expand Down
5 changes: 3 additions & 2 deletions tests/e2e_cuj/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Each CUJ owns a separate workspace. Subclass `BaseCujTest` from `base.py` and se
`UG_CUJ_SP_CLIENT_ID` and `UG_CUJ_SP_CLIENT_SECRET` with OAuth M2M authentication.
Do not share the workspace between concurrent runs. No stubbed configuration.

Run from the repository root:
Run from the repository root with Claude Code, Codex, and Databricks CLI installed:
`uv run pytest --confcutdir=tests/e2e_cuj tests/e2e_cuj`.
This keeps the parent suite's mocked fixtures out of CUJs. There are no tests yet.
This keeps the parent suite's mocked fixtures out of CUJs. Set
`UG_CUJ_SP_CLIENT_ID` and `UG_CUJ_SP_CLIENT_SECRET` for the dedicated workspace.
117 changes: 117 additions & 0 deletions tests/e2e_cuj/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
"""Fixtures for dedicated-workspace CUJs."""

from __future__ import annotations

import os
import re
import shutil
import sys
import tempfile
from pathlib import Path

import pytest

_INTEGRATION = Path(__file__).parents[1] / "integration"
_ROOT = _INTEGRATION.parents[1]
_SUITE = Path(__file__).parent
for _path in (_ROOT, _INTEGRATION, _SUITE):
if str(_path) not in sys.path:
sys.path.insert(0, str(_path))

from base import BaseCujTest # noqa: E402
from utils.harness import UserSession # noqa: E402


def _ug_binary() -> Path:
raw = os.environ.get("UG_INTEGRATION_BIN", "").strip()
if raw:
path = Path(raw).expanduser()
if not path.is_file():
pytest.fail(f"UG_INTEGRATION_BIN is not a file: {path}", pytrace=False)
return path

checkout_binary = _ROOT / ".venv" / ("Scripts/ug.exe" if os.name == "nt" else "bin/ug")
if checkout_binary.is_file():
return checkout_binary
pytest.fail(
f"No checkout ug found at {checkout_binary}; set UG_INTEGRATION_BIN to an installed ug.",
pytrace=False,
)


@pytest.fixture(scope="session")
def installed_binary():
return _ug_binary()


@pytest.fixture
def session(request, installed_binary):
case = re.sub(r"[^a-zA-Z0-9_.-]", "_", request.node.name)
with tempfile.TemporaryDirectory(prefix="ug-cuj-run-", dir=_ROOT) as root_name:
root = Path(root_name)
project_parent = root if os.name == "nt" else None
with tempfile.TemporaryDirectory(
prefix="ug-cuj-project-", dir=project_parent
) as project_name:
user = UserSession(
root,
Path(project_name),
installed_binary,
root / "artifacts" / case,
)
if os.name == "nt":
app_data = user.home / "AppData/Roaming"
local_app_data = user.home / "AppData/Local"
case_temp = local_app_data / "Temp"
for path in (app_data, local_app_data, case_temp):
path.mkdir(parents=True, exist_ok=True)
user.env.update(
{
"APPDATA": str(app_data),
"LOCALAPPDATA": str(local_app_data),
"TEMP": str(case_temp),
"TMP": str(case_temp),
"TMPDIR": str(case_temp),
}
)
try:
yield user
finally:
if any(
(user.home / ".ucode" / name).is_file()
for name in ("state.json", "managed-backups/manifest.json")
):
if os.name == "posix":
from utils.terminal import TerminalProcess

with TerminalProcess(
user, "ug", [str(user.binary), "revert"], "cleanup-revert"
) as terminal:
terminal.finish()
else:
user.run("revert", timeout=120)


@pytest.fixture
def live_session(request, session):
test = request.instance
if not isinstance(test, BaseCujTest):
pytest.fail("live_session requires a BaseCujTest instance.", pytrace=False)

for binary in ("databricks", "claude", "codex"):
if not shutil.which(binary, path=session.env["PATH"]):
pytest.fail(f"Required CUJ binary is missing: {binary}", pytrace=False)

has_sp_credentials = all(
os.environ.get(name, "").strip()
for name in ("UG_CUJ_SP_CLIENT_ID", "UG_CUJ_SP_CLIENT_SECRET")
)
if has_sp_credentials:
authorization = test.workspace.config.authenticate().get("Authorization", "")
bearer = authorization.removeprefix("Bearer ").strip()
else:
bearer = os.environ.get("DATABRICKS_BEARER", "").strip()
if not bearer:
pytest.fail("Workspace authentication returned no bearer token.", pytrace=False)
session.env["DATABRICKS_BEARER"] = bearer
return session
Loading
Loading