Repository navigation
[l] [agentbricks] Add pytest E2E suites - #706
elainewang-db merged 48 commits into
Conversation
Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
…backend seam Co-authored-by: Isaac <no-reply@databricks.com>
…ce to tools matrix Co-authored-by: Isaac <no-reply@databricks.com>
…ng matrices Co-authored-by: Isaac <no-reply@databricks.com>
… e2e matrices Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
…ver cleanup via --cleanup-only Co-authored-by: Isaac <no-reply@databricks.com>
… wheel build Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
…tool-name limit Co-authored-by: Isaac <no-reply@databricks.com>
…e-space-id, drop per-option env vars Co-authored-by: Isaac <no-reply@databricks.com>
…dbox with default user auth Co-authored-by: Isaac <no-reply@databricks.com>
…y off the CLI wrapper Co-authored-by: Isaac <no-reply@databricks.com>
…owns cleanup Co-authored-by: Isaac <no-reply@databricks.com>
…t.py Co-authored-by: Isaac <no-reply@databricks.com>
…; make pin_runtime a Project method Co-authored-by: Isaac <no-reply@databricks.com>
… drop unused App waits Co-authored-by: Isaac <no-reply@databricks.com>
…v and test_deploy Co-authored-by: Isaac <no-reply@databricks.com>
…sweep deletes the rest Co-authored-by: Isaac <no-reply@databricks.com>
…he project fixture Co-authored-by: Isaac <no-reply@databricks.com>
…ent.toml Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
…e prefix Co-authored-by: Isaac <no-reply@databricks.com>
…pp roles Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
…inspecting them Co-authored-by: Isaac <no-reply@databricks.com>
…eploys once Co-authored-by: Isaac <no-reply@databricks.com>
…ts dev once Co-authored-by: Isaac <no-reply@databricks.com>
…e the UC tool by purpose Co-authored-by: Isaac <no-reply@databricks.com>
jamesbxwu
left a comment
There was a problem hiding this comment.
thanks for addressing the comments! new structure is much cleaner. I found a few examples where we are still leaking internal details.
From a test organization PoV, since this is for the CLI, can we have one test file per command group? (e.g. test_init.py test_dev.py, test_deploy, test_tools.py etc). It makes it easier to map from our commands to test files.
| def assert_app_absent(self, name: str) -> None: | ||
| try: | ||
| self.client.apps.get(name) | ||
| except NotFound: | ||
| return | ||
| except DatabricksError as exc: | ||
| raise E2EError(f"Could not verify App {name} is absent: {exc}") from exc | ||
| raise E2EError(f"App {name} already exists; refusing to deploy over it.") |
There was a problem hiding this comment.
instead of this, can we use agentbricks deployment get to instead which stays at the CLI interface boundary?
|
|
||
| # Workarounds for the end-of-run cleanup: what `agentbricks deployments delete` leaves behind. | ||
|
|
||
| def wait_until_app_gone(self, name: str, timeout: float = 1200) -> None: |
There was a problem hiding this comment.
can this be replaced with agentbricks deployments get?
| `deployments delete` returns while the App is still deleting, so a teardown's App can still be | ||
| listed here; deleting it again only warns, and the wait is what decides success. |
There was a problem hiding this comment.
oh interesting, should deployments delete actually ensures it only returns once it's fully deleted?
There was a problem hiding this comment.
yes, I have a follow up pr for deployments delete improvements that I will add this to and drop from the test harness in that PR
| from harness.workspace import Workspace, app_resources | ||
|
|
||
|
|
||
| def test_tracing_experiment_bound( |
There was a problem hiding this comment.
this test is not an e2e testing test to me. IMO it should be the following and we don't need to verify/care about app resources. (note we can break up my example into smaller test cases if needed)
- init
- dev
- tracing get --experiment-name DEFAULT_EXP_FROM_INIT
- deploy + invoke
- tracing get
- unbind
- deploy + invoke
- tracing get (verify no tracing)
- bind --experiment NEW_EXPERIMENT
- deploy + invoke
- tracing get --experiment-name NEW_EXPERIMENT
There was a problem hiding this comment.
yep, definitely need to improve test coverage across all our resources, will create more in-depth follow ups for tracing as well as session store + memory store
There was a problem hiding this comment.
should we rename to test_tool_selection.py?
There was a problem hiding this comment.
why have different folders for memory_store, session_store, tools and tracing? does it make sense to flatten all into suites? (I noticed deploy/dev tests are under tools folder)
There was a problem hiding this comment.
they are the deploy/dev tests for the tools tests
…ot the Apps API Co-authored-by: Isaac <no-reply@databricks.com>
…ough app_exists Co-authored-by: Isaac <no-reply@databricks.com>
…iles Co-authored-by: Isaac <no-reply@databricks.com>
…odule Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
… store names from deploy Co-authored-by: Isaac <no-reply@databricks.com>
… differently Co-authored-by: Isaac <no-reply@databricks.com>
…n to its registered cleanup Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
jamesbxwu
left a comment
There was a problem hiding this comment.
thanks for addressing the comments, the remaining ones are minor.
Maybe I missed it but did we get a completed live e2e run? if so, can we include it in the PR?
| cleanup(f"memory stores delete memory-stores/{deployed['memory_store']} --yes") | ||
| cleanup(f"sessions stores delete {deployed['session_store']} --yes") |
There was a problem hiding this comment.
why do we delete the session/memory store after deploy?
There was a problem hiding this comment.
cleanup registers the cli delete commands so that they will run after the test regardless of whether the test succeeds or fails. but they need to be registered after deploy bc thats where we're returning the created session and memory store names
| agentbricks_cli.run(f"init --framework langgraph {project.path}", timeout=600) | ||
| project.pin_runtime() | ||
| app = project.app_name | ||
| # Before deploy: a deploy that fails partway can still leave the App behind. | ||
| cleanup(f"deployments delete {app} --yes") |
There was a problem hiding this comment.
i see this setup and cleanup being repeated across multiple tests, does it make sense to refactor?
There was a problem hiding this comment.
I think as long as the tests stay small and clean, its nice to see exactly what cli commands are run per test in each file, but we can move to a shared test helper if it's too much repeated code, wdyt? I initially had this in a helper but decided to unwrap it so all the cli commands can be read easily in the tests
| while process.poll() is None: | ||
| elapsed = time.monotonic() - started | ||
| if elapsed >= timeout: | ||
| terminate_process_group(process, grace=0) |
There was a problem hiding this comment.
[P2] Wait for timed-out commands to exit before teardown
Could we wait for the process group to stop before raising here? With grace=0, terminate_process_group returns immediately after SIGTERM and skips the SIGKILL fallback. A command or child process that delays or ignores termination can continue provisioning while pytest tears resources down, potentially leaving resources after the sweep. A bounded graceful wait, followed by SIGKILL if needed and reaping the process, would make the timeout safe for teardown.
| def check_genie(serialized: str) -> None: | ||
| _successful( | ||
| serialized, | ||
| lambda r: "genie" in str(r.get("name")) and bool(r.get("content")), |
There was a problem hiding this comment.
[P2] Check the Genie payload's completion status
Could we validate the status inside the Genie response as well? GenieAgent.ask and poll return terminal failures (FAILED, CANCELLED, QUERY_RESULT_EXPIRED) and timeout results as ordinary payloads. The outer ToolMessage can therefore have status == "success" and nonempty content even when Genie produced no successful answer, so this assertion passes. Please require a completed ask/poll response with answer content, and handle an in-progress response through polling if that is part of this scenario.
Co-authored-by: Isaac <no-reply@databricks.com>
…quire a completed Genie answer Co-authored-by: Isaac <no-reply@databricks.com>
Summary
Adds pytest E2E suites for the Agent Bricks CLI under
integrations/agentbricks/tests/, to replacetests/e2e/tool_matrix.py. They sit at the top of the test pyramid: one happy-path scenario per CLI command group, each running the real CLI and checking the agent it produces. Every step under test is the command a developer would type:endpoint invokereturns. They don't readagent.tomlor depend on CLI internals. The SDK is used only for what isn't under test (UC objects, warehouse, Genie) and for cleanup the CLI can't do yet.cleanupfixture (liket.Cleanupin databricks/cli). An end-of-run sweep deletes anything left.--workspace live|fakeseam;fakeis a stub for now.The old script stays until the nightly runner switches over.
tests/suites/README.mdcovers running, cleanup, layout, and adding a tool or command group.Tests (
tests/suites/, one file per command group)test_tools.pytools addthentools removeeach tool kind →dev→ asking for each tool, the agent no longer has ittest_dev.pyinit→tools addevery tool →dev→endpoint invokeeach tooltest_deploy.pydeploytest_memory.pydeploy→ remember a random codename under one actor → in a new session, therecalltool returns ittest_sessions.pydeploy→ tell the agent a random word → the same session answers with it, a different session doesn'ttest_tracing.pydeploy→endpoint invoke→tracing listshows a trace in the project's experimentThe tools are sandbox, MCP (web search), Python, UC function and Genie (skipped without
--genie-space-id), defined intool_definitions.py. A tool passes only if the response contains its own successful tool step with the expected result: the random marker in the sandbox's volume file, the Python or UC function's marker, aweb_searchresult with anhttpslink, or a Genie answer with statusCOMPLETED, since Genie reports a failed answer inside a successful tool step. The model writing about the tool isn't enough. The tests check no grants: a tool that answers shows deploy gave the agent the access it needs.--modelsets the model every invocation asks for, since workspaces name models differently (system.ai.gpt-5-2on production,system.ai.databricks-gpt-5-2on staging).endpoint invokeis retried only on a 5xx (3 attempts, 15s apart), which a new App can return while its session store comes up.Harness (
tests/harness/)agentbricks_cli.pyagentbrickscommands (run, andstartfordev), logging each tologs/<test>/project.pypin_runtime()cleanup.pyCleanup, theapps.jsonlrecord, and the end-of-run sweepworkspace.pytarget.py,run.py,logs.pyCleanup
When a test ends, its registered commands run, newest first; they are the only per-test deletes. Deploying tests register
deployments deletebefore deploying, so a deploy that fails partway is covered, and the store deletes right after, from the namesdeploy --output jsonreports ([agentbricks] Report the deployed memory and session stores in deploy --output json #760):Something already gone counts as deleted; other failures only warn. Before they run, the
projectfixture records the App's principal and Lakebase branch inapps.jsonl, saves a failed test's App logs, and fails a test that never calledpin_runtime().When the run ends, the sweep deletes everything carrying the run's name prefix (stores, Apps, synced source folders, tracing experiments, schemas), plus the Apps' Lakebase roles from
apps.jsonl. A failure here fails the run.After a killed run,
pytest tests/suites --cleanup-only --run-id <id> ...runs the sweep alone, for analways()CI step.Three sweep steps are workarounds for what nothing deletes yet: the App's synced source folder, its Lakebase role (
agents-<principal>), and the tracing experimentdeploycreates under/Shared/agentbricks_traces/.Testing
e5b2658): ruff andtypass; 6 tests collected;--workspace fakeskips all 6.in04: all 6 tests pass in 5 min 52 s, on the head's code (e5b2658), with [agentbricks] Keep the databricks CLI's own output out of --output json stdout #762 applied before it merged:test_deploy(sandbox, MCP, Python, UC function),test_memory,test_sessions,test_tracing,test_devandtest_tools_remove. The Genie tool was skipped (no--genie-space-id). A second--cleanup-only --run-id in04found nothing left. Without [agentbricks] Keep the databricks CLI's own output out of --output json stdout #762, every deploy test failed to parsedeploy --output json(runin01).gn01), with--genie-space-idfor the workspace's "Databricks AI Bridge Integration Test" space:test_devandtest_deploypass with all 5 tools in 5 min 39 s. Both runs'genie_askreturned statusCOMPLETEDwith 2 attachments. The deployed App's first call returned 502 twice while it started; the 5xx retry covered it.test_tools_remove, live (rundv13): aftertools remove, each prompt's response had no tool steps, and the agent said it doesn't have the tool.dv12:test_dev(4 tools, without--genie-space-id) andtest_toolspass in 2 min 38s, with no cleanup warnings. Intest_tools, none of the removed tools' prompts got a tool step, and the agent replied that the tool isn't available.ml01it deleted the 3 memory stores, 3 session stores (listing across pages) and 3 experiments the failed deploys left; on an older run it deleted that run's 8 experiments.pytest tests/unit_testspasses on main with [agentbricks] Report the deployed memory and session stores in deploy --output json #760's telemetry fix; this PR only touchestests/suitesandtests/harness.Issues found while building this, fixed here or in the CLI PRs it depends on:
VIRTUAL_ENVleaking intoapps run-local(#714); a staleapp.agentbricksdev.yamlafter SIGTERM (#726); the coding-agent hint corrupting--output jsonerrors (#718, the suite setsAGENTBRICKS_DISABLE_AGENT_HINT=1); CLI usage telemetry counting test runs as product usage (the suite setsAGENTBRICKS_DISABLE_TELEMETRY=1); SDK auth picking the wrong profile when two share a host; an inheritedPORToverriding the dev port; the 64-character MCP tool-name limit for UC functions (use a catalog of about 38 characters or fewer); and deploy requiringauthon every MCP, sandbox and Genie entry once any tool usesauth = "user".Follow-ups
databricks-internal-custom-agents/production) filled its 700 roles, so deploys failed; after the cleanup on the ticket it has 98. Most of it is likely per-databasedatabricks_reader_*/databricks_writer_*roles Lakebase doesn't drop when a database is deleted; the API protects them. LKB-12168, an in-progress Lakebase epic, removes these roles.DeleteRuntimeStoreleaves the App's role (agents-<principal>); the server creates it, so the server should drop it.deployments deletehas no E2E test of its own yet; registered cleanup and the sweep use it, so a broken delete fails the run in the sweep. It should wait until the App is gone, remove the synced source folder (a CLI PR is in progress), and treat an App that doesn't exist as already deleted (today it errors: "Could not resolve the app's service principal for Runtime Store cleanup"). It also leaves the tracing experiment and the storesdeploycreated; whether it should remove those is a question for the Agent Bricks team, since anagent.tomlcan share them.tracing unbind, thentracing bindto a new experiment, each followed by deploy and invoke.--auth app, the sandbox was denied reading its volume in 5 of 7 deploys on tilefood, even withREAD_VOLUME,USE_SCHEMAandUSE_CATALOGeffective for the App's principal. The suite covers only user-auth sandbox until that's fixed.tools addwithout--authmeansuser, but anagent.tomlentry withoutauthruns as the App.SessionStoreClient.get_sessiondoesn't retry a truncated response, so a first message to a new deploy can return a 500.frameworkfixture (langgraph,openai), with a per-framework reader for tool steps; the fake workspace, so the suites can run in PR CI; then switch the nightly runner to these suites and removetests/e2e/tool_matrix.py.This pull request and its description were written by Isaac.