diff --git a/.github/scripts/slow_test_durations.py b/.github/scripts/slow_test_durations.py new file mode 100644 index 000000000..294f59c2e --- /dev/null +++ b/.github/scripts/slow_test_durations.py @@ -0,0 +1,62 @@ +"""Rewrite tst/integration/slow_durations.json from the slow tier's JUnit reports, so the CI shards stay balanced. + + python .github/scripts/slow_test_durations.py junit-integration-slow-*.xml + +Download the reports from a green ``main`` run (the ``junit-integration-local-*`` artifacts of every +``integration-local-slow`` and ``integration-local-gateway`` shard). Each test's time counts toward its file, +or toward the test itself for a file the table names in ``split_by_test``; that list is kept as it is. +""" + +from __future__ import annotations + +import argparse +import json +import sys +import xml.etree.ElementTree as ET +from collections import defaultdict +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +TABLE = ROOT / "tst" / "integration" / "slow_durations.json" + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) + parser.add_argument("reports", nargs="+", help="JUnit XML reports written by pytest --junitxml") + parser.add_argument("--table", type=Path, default=TABLE, help="the durations table to rewrite") + parser.add_argument("--root", type=Path, default=ROOT, help="the checkout the reports' test files are in") + args = parser.parse_args() + + table = json.loads(args.table.read_text()) + split = set(table.get("split_by_test", [])) + seconds: dict[str, float] = defaultdict(float) + for report in args.reports: + for case in ET.parse(report).getroot().iter("testcase"): + nodeid = _nodeid(case.get("classname", ""), case.get("name", ""), args.root) + if nodeid is None: + print(f"slow test durations: no test file for {case.get('classname')!r}; left out") + continue + path = nodeid.split("::", 1)[0] + seconds[nodeid if path in split else path] += float(case.get("time") or 0) + if not seconds: + print("slow test durations: the reports have no test cases; the table is unchanged") + return 1 + table["seconds"] = {unit: round(value, 1) for unit, value in sorted(seconds.items())} + args.table.write_text(json.dumps(table, indent=1) + "\n") + print(f"slow test durations: {len(seconds)} units, {sum(seconds.values()) / 60:.1f} min, written to {args.table}") + return 0 + + +def _nodeid(classname: str, name: str, root: Path) -> str | None: + """The pytest node id of a JUnit test case: the longest dotted prefix of its classname that is a test + file, then any classes, then the test's name.""" + parts = classname.split(".") + for cut in range(len(parts), 0, -1): + path = "/".join(parts[:cut]) + ".py" + if (root / path).is_file(): + return "::".join([path, *parts[cut:], name]) + return None + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/local-backends.yml b/.github/workflows/local-backends.yml index b28a6da97..3239618f9 100644 --- a/.github/workflows/local-backends.yml +++ b/.github/workflows/local-backends.yml @@ -108,17 +108,16 @@ jobs: # The slow tier: builds the env images and the echo A2A agent (tst/data/a2a_agent) and deploys # them on the runner's Docker. The tests that need a real model, a remote sandbox, a registered # default A2A agent or MCP server sources from outside this repository skip with the capability reason. - # The gateway tests take as long as the rest of the tier, so they run beside it as a check of their own. - integration-local-slow: - name: ${{ matrix.check }} + # Its tests run serially, since they share image tags and artifact ids, so it is split across runners + # instead: AGENTENV_TEST_SHARD picks each runner's share, balanced by tst/integration/slow_durations.json + # (tst/util/shards.py). The gateway tests are a check of their own, split the same way. Each check passes + # when all of its shards do (the integration-local-slow and integration-local-gateway jobs below). + integration-local-slow-shard: + name: integration-local-slow (${{ matrix.shard }}/4) strategy: fail-fast: false matrix: - include: - - check: integration-local-slow - tests: tst/integration --ignore=tst/integration/env/gateway/gateway_test.py - - check: integration-local-gateway - tests: tst/integration/env/gateway/gateway_test.py + shard: [1, 2, 3, 4] runs-on: ubuntu-latest timeout-minutes: 40 steps: @@ -140,14 +139,85 @@ jobs: docker run --detach --name registry --publish 5000:5000 mirror.gcr.io/library/registry:2 timeout 30 sh -c 'until curl -sf http://localhost:5000/v2/ >/dev/null; do sleep 1; done' - name: Slow integration tier on the local defaults + env: + AGENTENV_TEST_SHARD: ${{ matrix.shard }}/4 + run: | + .venv/bin/python -m pytest -m int_test_slow -rA --timeout=1200 --junitxml=junit-integration-slow.xml \ + tst/integration --ignore=tst/integration/env/gateway/gateway_test.py + - name: Skips must be declared capability gaps + if: always() + run: | + .venv/bin/python .github/scripts/check_skip_policy.py junit-integration-slow.xml \ + --allow model_endpoint_configured remote_sandbox default_a2a_agent mcp_server_sources + # The tests' times, for .github/scripts/slow_test_durations.py to rebalance the shards with. + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + if: always() + with: + name: junit-integration-local-slow-${{ matrix.shard }} + path: junit-integration-slow.xml + retention-days: 14 + + integration-local-gateway-shard: + name: integration-local-gateway (${{ matrix.shard }}/2) + strategy: + fail-fast: false + matrix: + shard: [1, 2] + runs-on: ubuntu-latest + timeout-minutes: 40 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.12" + - name: Install from uv.lock + run: | + python -m pip install --quiet -r .github/ci-requirements.txt + uv sync --locked --extra dev --python "$(command -v python)" + echo "$PWD/.venv/bin" >> "$GITHUB_PATH" + - name: Pull the base images from mirror.gcr.io + run: python3 .github/scripts/mirror_base_images.py src tst + - name: Start the local registry + run: | + docker run --detach --name registry --publish 5000:5000 mirror.gcr.io/library/registry:2 + timeout 30 sh -c 'until curl -sf http://localhost:5000/v2/ >/dev/null; do sleep 1; done' + - name: Slow integration tier on the local defaults + env: + AGENTENV_TEST_SHARD: ${{ matrix.shard }}/2 run: | .venv/bin/python -m pytest -m int_test_slow -rA --timeout=1200 --junitxml=junit-integration-slow.xml \ - ${{ matrix.tests }} + tst/integration/env/gateway/gateway_test.py - name: Skips must be declared capability gaps if: always() run: | .venv/bin/python .github/scripts/check_skip_policy.py junit-integration-slow.xml \ --allow model_endpoint_configured remote_sandbox default_a2a_agent mcp_server_sources + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + if: always() + with: + name: junit-integration-local-gateway-${{ matrix.shard }} + path: junit-integration-slow.xml + retention-days: 14 + + # One check per tier under its own name (the main ruleset requires integration-local-slow): each passes + # when every shard of its tier passed. + integration-local-slow: + if: always() + needs: integration-local-slow-shard + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Every shard passed + run: test "${{ needs.integration-local-slow-shard.result }}" = success + + integration-local-gateway: + if: always() + needs: integration-local-gateway-shard + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Every shard passed + run: test "${{ needs.integration-local-gateway-shard.result }}" = success # Whether this change touches the code the installer tier covers; the tier costs minutes, so # it runs only then. A skipped job reports as passed to a required check. diff --git a/AGENTS.md b/AGENTS.md index 19b082bef..5eca2721e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -252,7 +252,11 @@ orchestrators call it the same way. What callers rely on: `_disable_network` fixture in a closer conftest when a loopback server is the point. - `tst/integration/` needs Docker and the local registry on `:5000` and runs on the local default backends. Minutes-long tests (real image builds, sandbox VMs, gateways, agents) carry - `@pytest.mark.int_test_slow` (or module-level `pytestmark`) and run serially. + `@pytest.mark.int_test_slow` (or module-level `pytestmark`) and run serially. CI splits them across + runners instead: `AGENTENV_TEST_SHARD=i/N` (`tst/util/shards.py`) runs the i-th of N groups, keeping a + file's tests together and balancing the groups by `tst/integration/slow_durations.json`. When the + shards' times drift apart, rewrite the table from a `main` run's `junit-integration-local-*` + artifacts with `.github/scripts/slow_test_durations.py`. - `tst/manual/` and `tst/benchmarks/` run only by hand, as each module's docstring says; no CI job collects them. - Skips are declared capability gaps, decided at collection time: the gates in @@ -277,7 +281,9 @@ resolved, and the checks the `main` ruleset requires: `unit`, `integration-local pass, but a failing one does not block the merge. `.github/workflows/local-backends.yml` runs `unit`, `ui`, `integration-local`, `integration-local-slow`, `integration-local-gateway` (the slow tier's `gateway_test.py`, run beside the rest of it) and -`installer`. The Python jobs install from `uv.lock` (public PyPI only), with the uv that +`installer`. The slow tier runs on four runners (`integration-local-slow (1/4)` to `(4/4)`) and the +gateway tests on two, and `integration-local-slow` and `integration-local-gateway` each pass when all +of their shards do. The Python jobs install from `uv.lock` (public PyPI only), with the uv that `.github/ci-requirements.txt` pins, and use no secrets and no external services (a `registry:2` container for the integration jobs). The `unit` job also fails if `uv.lock` resolves anything from a registry other than PyPI, if `agentenv-framework-protocol` is not the editable workspace member, if diff --git a/Makefile b/Makefile index 17cf0ef0f..bf2ecb416 100644 --- a/Makefile +++ b/Makefile @@ -43,7 +43,10 @@ unit-test: ## Run the unit test suite in parallel (no external services) # "task-step-test-deploy-multi"), producing ERROR-at-setup cascades that # look like real bugs but are pure concurrency. Running serial costs wall # clock but is the only way to keep slow runs reliable without rewriting -# every module's fixtures with worker-unique tags. +# every module's fixtures with worker-unique tags. CI splits the slow tier +# across runners instead (tst/util/shards.py). To run one runner's share: +# AGENTENV_TEST_SHARD=2/4 make int-test-slow PYTEST_ARGS=--ignore=tst/integration/env/gateway/gateway_test.py +# AGENTENV_TEST_SHARD=1/2 .venv/bin/python -m pytest -m int_test_slow tst/integration/env/gateway/gateway_test.py INT_PYTEST_ARGS_FAST ?= --dist=loadscope --tb=short --durations=20 -p no:cacheprovider INT_PYTEST_ARGS_SLOW ?= --tb=short --durations=20 -p no:cacheprovider INT_FAST_WORKERS ?= auto diff --git a/tst/conftest.py b/tst/conftest.py index af537f562..e33f3a589 100644 --- a/tst/conftest.py +++ b/tst/conftest.py @@ -1,5 +1,6 @@ """Shared pytest fixtures for all tests.""" +import os import shutil import signal import sys @@ -9,7 +10,9 @@ from agent_env.config import reset_config from agent_env.store.document_store import document_store as document_store_module +from agent_env.store.object_store.local import grant_server as grant_server_module from agent_env.store.routing import disable_namespace_routing +from tst.util import shards def _reset_store_singletons() -> None: @@ -40,6 +43,29 @@ def pytest_unconfigure(config): shutil.rmtree(config.stash[_STATE_DIR], ignore_errors=True) +@pytest.hookimpl(trylast=True) +def pytest_collection_modifyitems(config, items): + """With ``AGENTENV_TEST_SHARD=i/N``, run only the i-th of N groups of the selected tests (``tst/util/shards.py``). + Last, so it splits what ``-m`` and the other filters left.""" + value = os.environ.get(shards.ENV_VAR) + if not value: + return + try: + index, count = shards.parse(value) + except ValueError as e: + raise pytest.UsageError(str(e)) from None + table = shards.load_table() + marked = [(item, item.get_closest_marker("dependency")) for item in items] + linked = [(item.nodeid, mark.kwargs.get("name") or item.name, list(mark.kwargs.get("depends") or [])) + for item, mark in marked if mark is not None] + together = shards.dependency_groups(linked, table) + keep = set(shards.select([item.nodeid for item in items], index, count, table, together)) + deselected = [item for item in items if item.nodeid not in keep] + if deselected: + config.hook.pytest_deselected(items=deselected) + items[:] = [item for item in items if item.nodeid in keep] + + @pytest.fixture def sigint_handled(): """SIGINT handled as Python handles it by default, for a test that sends it: a CI job started in the background @@ -82,8 +108,12 @@ def fresh_config_document(): @pytest.fixture(scope="module", autouse=True) def setup_config(): - """Per-module store-singleton sweep; the config itself is reset per test above.""" + """Per-module store-singleton sweep; the config itself is reset per test above. The grant servers close too: + one a module started under its own state root presents that root's CA, which the next module's containers + don't trust, and the next grant starts a server afresh.""" _reset_store_singletons() yield reset_config() _reset_store_singletons() + for server in list(grant_server_module._servers.values()): + server.close() diff --git a/tst/integration/slow_durations.json b/tst/integration/slow_durations.json new file mode 100644 index 000000000..e8374021f --- /dev/null +++ b/tst/integration/slow_durations.json @@ -0,0 +1,667 @@ +{ + "_comment": "Seconds each slow-tier unit took in CI, which AGENTENV_TEST_SHARD balances the CI shards by (tst/util/shards.py). A unit is a file, or a test of a file in split_by_test. Refresh with .github/scripts/slow_test_durations.py from the shards' JUnit reports.", + "split_by_test": [ + "tst/integration/env/gateway/gateway_test.py" + ], + "seconds": { + "tst/integration/agent/a2a_agent/a2a_agent_test.py": 0.0, + "tst/integration/agent/a2a_agent/agent_triggers_e2e_test.py": 0.0, + "tst/integration/agent/proxy_modal_vm_test.py": 13.6, + "tst/integration/artifact/artifacts/docker_image_artifact_test.py": 7.2, + "tst/integration/cli/bundle_init_images_test.py": 23.4, + "tst/integration/cli/cli_test.py": 58.3, + "tst/integration/cli/run_bundle_built_agents_test.py": 36.9, + "tst/integration/cli/run_bundle_built_envs_test.py": 534.3, + "tst/integration/cli/run_bundle_claude_code_install_test.py": 156.8, + "tst/integration/cli/run_bundle_claude_code_test.py": 90.6, + "tst/integration/cli/run_bundle_cli_folder_test.py": 74.7, + "tst/integration/cli/run_bundle_image_folder_test.py": 25.9, + "tst/integration/cli/run_bundle_image_ref_test.py": 14.1, + "tst/integration/cli/run_bundle_installed_agent_test.py": 8.2, + "tst/integration/cli/run_bundle_openhands_test.py": 228.5, + "tst/integration/cli/run_bundle_skills_test.py": 12.8, + "tst/integration/cli/run_role_models_test.py": 78.8, + "tst/integration/computer/recording_chaos_test.py": 74.6, + "tst/integration/computer/scaffold_test.py": 51.7, + "tst/integration/env/env_test.py": 4.1, + "tst/integration/env/envs/build_context_local_test.py": 170.2, + "tst/integration/env/envs/multi_env_local_test.py": 52.6, + "tst/integration/env/envs/server_env_modal_test.py": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_airtable_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_airtable_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_airtable_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_airtable_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_airtable_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_airtable_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_airtable_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_airtable_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_amazon_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_amazon_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_amazon_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_amazon_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_amazon_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_amazon_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_amazon_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_amazon_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_amazon_fresh_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_amazon_fresh_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_amazon_fresh_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_amazon_fresh_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_amazon_fresh_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_amazon_fresh_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_amazon_fresh_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_amazon_fresh_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_apple_health_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_apple_health_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_apple_health_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_apple_health_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_apple_health_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_apple_health_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_apple_health_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_apple_health_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_blackline_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_blackline_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_blackline_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_blackline_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_blackline_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_blackline_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_blackline_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_blackline_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_buildium_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_buildium_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_buildium_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_buildium_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_buildium_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_buildium_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_buildium_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_buildium_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_cab_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_cab_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_cab_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_cab_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_cab_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_cab_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_cab_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_cab_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_calendar_mcp_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_calendar_mcp_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_calendar_mcp_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_calendar_mcp_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_calendar_mcp_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_calendar_mcp_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_calendar_mcp_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_calendar_mcp_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_cardless_environment_resolves_name_from_environment_name": 38.8, + "tst/integration/env/gateway/gateway_test.py::test_clickup_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_clickup_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_clickup_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_clickup_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_clickup_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_clickup_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_clickup_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_clickup_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_confluence_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_confluence_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_confluence_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_confluence_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_confluence_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_confluence_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_confluence_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_confluence_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_crm_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_crm_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_crm_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_crm_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_crm_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_crm_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_crm_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_crm_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_eight_sleep_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_eight_sleep_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_eight_sleep_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_eight_sleep_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_eight_sleep_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_eight_sleep_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_eight_sleep_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_eight_sleep_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_email_mcp_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_email_mcp_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_email_mcp_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_email_mcp_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_email_mcp_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_email_mcp_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_email_mcp_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_email_mcp_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_env_validator_task": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_erp_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_erp_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_erp_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_erp_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_erp_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_erp_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_erp_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_erp_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_figma_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_figma_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_figma_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_figma_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_figma_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_figma_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_figma_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_figma_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_fintrack_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_fintrack_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_fintrack_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_fintrack_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_fintrack_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_fintrack_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_fintrack_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_fintrack_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_fitbit_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_fitbit_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_fitbit_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_fitbit_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_fitbit_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_fitbit_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_fitbit_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_fitbit_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_fresh_direct_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_fresh_direct_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_fresh_direct_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_fresh_direct_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_fresh_direct_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_fresh_direct_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_fresh_direct_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_fresh_direct_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_garmin_health_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_garmin_health_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_garmin_health_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_garmin_health_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_garmin_health_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_garmin_health_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_garmin_health_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_garmin_health_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_autonomous_time_trigger[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_autonomous_time_trigger[daytona]": 0.1, + "tst/integration/env/gateway/gateway_test.py::test_gateway_autonomous_time_trigger[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_autonomous_time_trigger[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_autonomous_time_trigger[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_autonomous_time_trigger[scale]": 29.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_autonomous_time_trigger[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_autonomous_time_trigger[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_barrier_orders_a_templated_mirror[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_barrier_orders_a_templated_mirror[daytona]": 0.1, + "tst/integration/env/gateway/gateway_test.py::test_gateway_barrier_orders_a_templated_mirror[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_barrier_orders_a_templated_mirror[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_barrier_orders_a_templated_mirror[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_barrier_orders_a_templated_mirror[scale]": 28.2, + "tst/integration/env/gateway/gateway_test.py::test_gateway_barrier_orders_a_templated_mirror[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_barrier_orders_a_templated_mirror[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_changes_name_the_request_behind_a_write_through_the_rest_proxy": 26.4, + "tst/integration/env/gateway/gateway_test.py::test_gateway_changes_name_the_role_behind_each_write": 26.8, + "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_extension[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_extension[daytona]": 0.1, + "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_extension[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_extension[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_extension[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_extension[scale]": 36.8, + "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_extension[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_extension[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_rate_accuracy[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_rate_accuracy[daytona]": 0.1, + "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_rate_accuracy[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_rate_accuracy[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_rate_accuracy[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_rate_accuracy[scale]": 51.1, + "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_rate_accuracy[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_rate_accuracy[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_consistent_mode": 41.2, + "tst/integration/env/gateway/gateway_test.py::test_gateway_queued_trigger_is_not_reported_settled[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_queued_trigger_is_not_reported_settled[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_queued_trigger_is_not_reported_settled[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_queued_trigger_is_not_reported_settled[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_queued_trigger_is_not_reported_settled[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_queued_trigger_is_not_reported_settled[scale]": 30.2, + "tst/integration/env/gateway/gateway_test.py::test_gateway_queued_trigger_is_not_reported_settled[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_queued_trigger_is_not_reported_settled[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_time_trigger_extension[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_time_trigger_extension[daytona]": 0.1, + "tst/integration/env/gateway/gateway_test.py::test_gateway_time_trigger_extension[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_time_trigger_extension[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_time_trigger_extension[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_time_trigger_extension[scale]": 28.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_time_trigger_extension[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_time_trigger_extension[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_rearms_after_a_failed_mirror[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_rearms_after_a_failed_mirror[daytona]": 0.1, + "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_rearms_after_a_failed_mirror[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_rearms_after_a_failed_mirror[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_rearms_after_a_failed_mirror[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_rearms_after_a_failed_mirror[scale]": 30.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_rearms_after_a_failed_mirror[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_rearms_after_a_failed_mirror[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_registration_is_fail_loud[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_registration_is_fail_loud[daytona]": 0.1, + "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_registration_is_fail_loud[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_registration_is_fail_loud[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_registration_is_fail_loud[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_registration_is_fail_loud[scale]": 26.1, + "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_registration_is_fail_loud[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_registration_is_fail_loud[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_triggers_extension[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_triggers_extension[daytona]": 0.1, + "tst/integration/env/gateway/gateway_test.py::test_gateway_triggers_extension[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_triggers_extension[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_triggers_extension[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_triggers_extension[scale]": 37.5, + "tst/integration/env/gateway/gateway_test.py::test_gateway_triggers_extension[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_triggers_extension[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_triggers_nl_executor[modal_vm]": 0.1, + "tst/integration/env/gateway/gateway_test.py::test_gateway_website_trigger_fires_on_a_request_through_the_website_proxy": 39.4, + "tst/integration/env/gateway/gateway_test.py::test_gateway_with_agentenv_environment": 28.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_with_multi_env_including_website": 47.9, + "tst/integration/env/gateway/gateway_test.py::test_gateway_with_multi_mcp_server[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_with_multi_mcp_server[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_with_multi_mcp_server[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_with_multi_mcp_server[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_with_multi_mcp_server[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_with_multi_mcp_server[scale]": 28.3, + "tst/integration/env/gateway/gateway_test.py::test_gateway_with_multi_mcp_server[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_with_multi_mcp_server[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_with_single_mcp_server": 189.2, + "tst/integration/env/gateway/gateway_test.py::test_gateway_with_single_website_env": 64.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_with_website_env_v1": 40.6, + "tst/integration/env/gateway/gateway_test.py::test_github_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_github_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_github_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_github_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_github_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_github_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_github_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_github_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gmail_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gmail_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gmail_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gmail_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gmail_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gmail_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gmail_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gmail_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gofundme_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gofundme_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gofundme_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gofundme_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gofundme_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gofundme_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gofundme_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_gofundme_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_calendar_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_calendar_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_calendar_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_calendar_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_calendar_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_calendar_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_calendar_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_calendar_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_docs_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_docs_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_docs_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_docs_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_docs_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_docs_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_docs_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_docs_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_drive_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_drive_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_drive_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_drive_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_drive_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_drive_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_drive_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_drive_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_sheets_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_sheets_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_sheets_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_sheets_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_sheets_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_sheets_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_sheets_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_sheets_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_slides_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_slides_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_slides_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_slides_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_slides_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_slides_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_slides_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_google_slides_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_hotel_management_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_hotel_management_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_hotel_management_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_hotel_management_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_hotel_management_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_hotel_management_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_hotel_management_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_hotel_management_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_instacart_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_instacart_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_instacart_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_instacart_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_instacart_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_instacart_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_instacart_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_instacart_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_jira_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_jira_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_jira_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_jira_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_jira_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_jira_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_jira_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_jira_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_linear_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_linear_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_linear_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_linear_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_linear_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_linear_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_linear_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_linear_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_load_from_snapshot": 41.8, + "tst/integration/env/gateway/gateway_test.py::test_messaging_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_messaging_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_messaging_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_messaging_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_messaging_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_messaging_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_messaging_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_messaging_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_mortgage_los_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_mortgage_los_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_mortgage_los_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_mortgage_los_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_mortgage_los_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_mortgage_los_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_mortgage_los_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_mortgage_los_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_myfitnesspal_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_myfitnesspal_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_myfitnesspal_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_myfitnesspal_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_myfitnesspal_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_myfitnesspal_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_myfitnesspal_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_myfitnesspal_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_notion_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_notion_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_notion_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_notion_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_notion_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_notion_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_notion_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_notion_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_obsidian_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_obsidian_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_obsidian_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_obsidian_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_obsidian_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_obsidian_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_obsidian_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_obsidian_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_oms_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_oms_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_oms_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_oms_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_oms_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_oms_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_oms_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_oms_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_oracle_gl_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_oracle_gl_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_oracle_gl_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_oracle_gl_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_oracle_gl_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_oracle_gl_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_oracle_gl_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_oracle_gl_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_polymarket_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_polymarket_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_polymarket_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_polymarket_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_polymarket_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_polymarket_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_polymarket_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_polymarket_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_quickbooks_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_quickbooks_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_quickbooks_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_quickbooks_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_quickbooks_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_quickbooks_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_quickbooks_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_quickbooks_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_records_vault_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_records_vault_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_records_vault_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_records_vault_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_records_vault_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_records_vault_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_records_vault_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_records_vault_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_reminder_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_reminder_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_reminder_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_reminder_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_reminder_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_reminder_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_reminder_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_reminder_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_renamed_registration_serves_its_registered_name": 29.2, + "tst/integration/env/gateway/gateway_test.py::test_renpho_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_renpho_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_renpho_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_renpho_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_renpho_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_renpho_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_renpho_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_renpho_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_ring_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_ring_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_ring_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_ring_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_ring_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_ring_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_ring_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_ring_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_sap_subledger_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_sap_subledger_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_sap_subledger_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_sap_subledger_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_sap_subledger_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_sap_subledger_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_sap_subledger_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_sap_subledger_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_shopping_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_shopping_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_shopping_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_shopping_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_shopping_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_shopping_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_shopping_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_shopping_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_slack_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_slack_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_slack_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_slack_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_slack_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_slack_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_slack_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_slack_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_smart_lock_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_smart_lock_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_smart_lock_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_smart_lock_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_smart_lock_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_smart_lock_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_smart_lock_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_smart_lock_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_snapshot_agent_state_v1_and_fallback": 27.4, + "tst/integration/env/gateway/gateway_test.py::test_snapshot_cli": 36.3, + "tst/integration/env/gateway/gateway_test.py::test_snowflake_async_virtual_clock[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_snowflake_async_virtual_clock[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_snowflake_async_virtual_clock[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_snowflake_async_virtual_clock[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_snowflake_async_virtual_clock[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_snowflake_async_virtual_clock[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_snowflake_async_virtual_clock[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_snowflake_async_virtual_clock[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_snowflake_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_snowflake_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_snowflake_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_snowflake_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_snowflake_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_snowflake_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_snowflake_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_snowflake_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_stayncook_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_stayncook_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_stayncook_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_stayncook_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_stayncook_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_stayncook_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_stayncook_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_stayncook_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_strava_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_strava_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_strava_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_strava_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_strava_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_strava_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_strava_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_strava_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_stripe_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_stripe_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_stripe_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_stripe_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_stripe_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_stripe_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_stripe_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_stripe_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_sync_env_clock_step[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_sync_env_clock_step[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_sync_env_clock_step[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_sync_env_clock_step[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_sync_env_clock_step[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_sync_env_clock_step[scale]": 26.6, + "tst/integration/env/gateway/gateway_test.py::test_sync_env_clock_step[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_sync_env_clock_step[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_system_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_system_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_system_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_system_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_system_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_system_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_system_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_system_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_target_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_target_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_target_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_target_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_target_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_target_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_target_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_target_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_ticketmaster_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_ticketmaster_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_ticketmaster_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_ticketmaster_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_ticketmaster_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_ticketmaster_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_ticketmaster_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_ticketmaster_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_tms_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_tms_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_tms_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_tms_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_tms_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_tms_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_tms_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_tms_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_trello_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_trello_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_trello_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_trello_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_trello_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_trello_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_trello_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_trello_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_validate_universe_compatibility": 43.4, + "tst/integration/env/gateway/gateway_test.py::test_verify_universe_roundtrip_export_all_v1_and_fallback": 27.3, + "tst/integration/env/gateway/gateway_test.py::test_walmart_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_walmart_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_walmart_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_walmart_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_walmart_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_walmart_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_walmart_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_walmart_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_weather_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_weather_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_weather_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_weather_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_weather_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_weather_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_weather_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_weather_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_whoop_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_whoop_clock_consumer[daytona]": 30.0, + "tst/integration/env/gateway/gateway_test.py::test_whoop_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_whoop_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_whoop_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_whoop_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_whoop_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_whoop_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_wms_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_wms_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_wms_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_wms_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_wms_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_wms_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_wms_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_wms_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_zendesk_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_zendesk_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_zendesk_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_zendesk_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_zendesk_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_zendesk_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_zendesk_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_zendesk_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_zillow_clock_consumer[boxd]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_zillow_clock_consumer[daytona]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_zillow_clock_consumer[modal]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_zillow_clock_consumer[modal_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_zillow_clock_consumer[sail_vm]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_zillow_clock_consumer[scale]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_zillow_clock_consumer[tenki]": 0.0, + "tst/integration/env/gateway/gateway_test.py::test_zillow_clock_consumer[tensorlake]": 0.0, + "tst/integration/env/task_step/task_steps_test.py": 111.6, + "tst/integration/eval/eval_test.py": 0.2, + "tst/integration/openenv/build_app_test.py": 42.4, + "tst/integration/providers/sandbox_providers/boxd_sandbox_smoke_test.py": 0.0, + "tst/integration/providers/sandbox_providers/daytona_sandbox_smoke_test.py": 0.0, + "tst/integration/providers/sandbox_providers/e2b_sandbox_smoke_test.py": 0.0, + "tst/integration/providers/sandbox_providers/sail_vm_sandbox_smoke_test.py": 0.0, + "tst/integration/providers/sandbox_providers/tenki_sandbox_smoke_test.py": 0.0, + "tst/integration/providers/sandbox_providers/tensorlake_sandbox_smoke_test.py": 0.0, + "tst/integration/providers/sandbox_providers/vercel_sandbox_smoke_test.py": 0.1, + "tst/integration/providers/sandbox_providers/vm_sandbox_build_test.py": 2.8, + "tst/integration/store/local_parallel_deploy_test.py": 51.8, + "tst/integration/task_step/test_agent_on_local_sandbox.py": 8.4, + "tst/integration/task_step/test_enable_docker_e2e.py": 0.0, + "tst/integration/task_step/test_multiturn_verifier_delivery.py": 0.2, + "tst/integration/task_step/test_next_env_event_hook_local.py": 111.6, + "tst/integration/task_step/test_register_env_triggers_local.py": 285.8, + "tst/integration/utils/docker/prune_test.py": 2.6 + } +} diff --git a/tst/unit/test_shards.py b/tst/unit/test_shards.py new file mode 100644 index 000000000..bcbf55061 --- /dev/null +++ b/tst/unit/test_shards.py @@ -0,0 +1,123 @@ +"""AGENTENV_TEST_SHARD (``tst/util/shards.py`` and its hook in ``tst/conftest.py``): the CI slow-tier shards.""" + +from __future__ import annotations + +import os +import subprocess +import sys +from pathlib import Path + +import pytest + +from tst.util import shards + +ROOT = Path(__file__).resolve().parents[2] +SPLIT = "tst/integration/env/gateway/gateway_test.py" + + +def _table(seconds: dict[str, float]) -> dict: + return {"split_by_test": [SPLIT], "seconds": seconds} + + +def _groups(nodeids: list[str], count: int, table: dict) -> list[list[str]]: + return [shards.select(nodeids, index, count, table) for index in range(1, count + 1)] + + +@pytest.mark.parametrize("value,expected", [("1/4", (1, 4)), ("4/4", (4, 4)), ("1/1", (1, 1))]) +def test_parse_reads_the_shard_and_the_count(value, expected): + assert shards.parse(value) == expected + + +@pytest.mark.parametrize("value", ["", "2", "0/4", "5/4", "a/4", "1/4/2"]) +def test_parse_refuses_anything_but_i_of_n(value): + with pytest.raises(ValueError, match=shards.ENV_VAR): + shards.parse(value) + + +def test_every_test_runs_in_exactly_one_shard_in_its_collected_order(): + nodeids = [f"tst/integration/f{n}_test.py::test_{t}" for n in range(7) for t in range(3)] + groups = _groups(nodeids, 3, _table({f"tst/integration/f{n}_test.py": 10 * n for n in range(7)})) + assert sorted(sum(groups, [])) == sorted(nodeids) + for group in groups: + assert group == [nodeid for nodeid in nodeids if nodeid in group] + + +def test_a_files_tests_stay_together_and_the_heaviest_files_go_to_different_shards(): + seconds = {"tst/integration/big_test.py": 600, "tst/integration/mid_test.py": 500, "tst/integration/small_test.py": 100} + nodeids = [f"{path}::test_{t}" for path in seconds for t in range(4)] + groups = _groups(nodeids, 2, _table(seconds)) + assert {nodeid.split("::")[0] for nodeid in groups[0]} == {"tst/integration/big_test.py"} + assert {nodeid.split("::")[0] for nodeid in groups[1]} == {"tst/integration/mid_test.py", "tst/integration/small_test.py"} + + +def test_a_file_named_in_split_by_test_is_split_by_test(): + nodeids = [f"{SPLIT}::test_{t}" for t in range(4)] + groups = _groups(nodeids, 2, _table({nodeid: 60 for nodeid in nodeids})) + assert [len(group) for group in groups] == [2, 2] + + +def test_tests_a_dependency_links_stay_on_one_shard_and_weigh_their_sum(): + nodeids = [f"{SPLIT}::test_{name}" for name in ("save", "load", "a", "b")] + table = _table({f"{SPLIT}::test_save": 50, f"{SPLIT}::test_load": 50, f"{SPLIT}::test_a": 60, f"{SPLIT}::test_b": 40}) + linked = [(f"{SPLIT}::test_save", "save", []), (f"{SPLIT}::test_load", "test_load", ["save"])] + together = shards.dependency_groups(linked, table) + assert together == {f"{SPLIT}::test_save": f"{SPLIT}::test_load", f"{SPLIT}::test_load": f"{SPLIT}::test_load"} + groups = [shards.select(nodeids, index, 2, table, together) for index in (1, 2)] + # save+load (100) on one shard, a (60) and b (40) on the other + assert sorted(groups, key=len) == [[f"{SPLIT}::test_save", f"{SPLIT}::test_load"], + [f"{SPLIT}::test_a", f"{SPLIT}::test_b"]] + + +def test_a_dependency_outside_a_split_file_or_on_an_unknown_name_links_nothing(): + table = _table({}) + linked = [("tst/integration/whole_test.py::test_load", "test_load", ["save"]), + ("tst/integration/whole_test.py::test_save", "save", []), + (f"{SPLIT}::test_load", "test_load", ["missing"])] + assert shards.dependency_groups(linked, table) == {} + + +def test_a_unit_the_table_does_not_know_weighs_the_median_of_the_known_ones(): + seconds = {"tst/integration/a_test.py": 10, "tst/integration/b_test.py": 20, "tst/integration/c_test.py": 900, + "tst/integration/skipped_test.py": 0} + nodeids = [f"{path}::test" for path in [*seconds, "tst/integration/new_test.py"]] + groups = _groups(nodeids, 2, _table(seconds)) + # c (900) alone on one shard; the rest, the new file weighing the median of 10, 20 and 900, on the other + assert groups[0] == ["tst/integration/c_test.py::test"] + + +def test_the_same_table_and_tests_always_give_the_same_shards(): + nodeids = [f"tst/integration/f{n}_test.py::test" for n in range(12)] + table = _table({f"tst/integration/f{n}_test.py": 30 for n in range(12)}) + assert _groups(nodeids, 4, table) == _groups(list(nodeids), 4, table) + + +def test_the_committed_table_names_a_file_for_every_slow_tier_unit_it_splits(): + table = shards.load_table() + assert table["split_by_test"] == [SPLIT] + assert all((ROOT / unit.split("::")[0]).is_file() for unit in table["seconds"]), "the table names a test file that is gone" + + +def _collect(*paths: str, shard: str | None) -> subprocess.CompletedProcess: + env = {key: value for key, value in os.environ.items() if key != shards.ENV_VAR} + if shard is not None: + env[shards.ENV_VAR] = shard + return subprocess.run([sys.executable, "-m", "pytest", "--collect-only", "-q", "-p", "no:cacheprovider", *paths], + cwd=ROOT, env=env, capture_output=True, text=True) + + +def _collected(result: subprocess.CompletedProcess) -> set[str]: + return {line for line in result.stdout.splitlines() if "::" in line} + + +def test_the_hook_splits_a_collection_into_shards_that_together_are_all_of_it(): + paths = ("tst/unit/test_check_skip_policy.py", "tst/unit/test_tls_verification.py") + everything = _collected(_collect(*paths, shard=None)) + first, second = (_collected(_collect(*paths, shard=f"{index}/2")) for index in (1, 2)) + assert first and second and not first & second + assert first | second == everything + + +def test_the_hook_refuses_a_malformed_shard(): + result = _collect("tst/unit/test_tls_verification.py", shard="3/2") + assert result.returncode == pytest.ExitCode.USAGE_ERROR + assert shards.ENV_VAR in result.stderr diff --git a/tst/unit/test_slow_test_durations_script.py b/tst/unit/test_slow_test_durations_script.py new file mode 100644 index 000000000..80033f28e --- /dev/null +++ b/tst/unit/test_slow_test_durations_script.py @@ -0,0 +1,69 @@ +"""The slow-tier durations refresh in ``.github/scripts/slow_test_durations.py``.""" + +from __future__ import annotations + +import json +import subprocess +import sys +from pathlib import Path + +SCRIPT = Path(__file__).resolve().parents[2] / ".github" / "scripts" / "slow_test_durations.py" +SPLIT = "tst/integration/env/gateway/gateway_test.py" + + +def _checkout(tmp_path: Path) -> Path: + for path in ("tst/integration/cli/cli_test.py", "tst/integration/env/task_step/task_steps_test.py", SPLIT): + (tmp_path / path).parent.mkdir(parents=True, exist_ok=True) + (tmp_path / path).write_text("") + table = tmp_path / "slow_durations.json" + table.write_text(json.dumps({"_comment": "kept", "split_by_test": [SPLIT], "seconds": {"tst/integration/gone_test.py": 9}})) + return table + + +def _report(tmp_path: Path, name: str, cases: list[tuple[str, str, float]]) -> Path: + body = "".join(f'' for classname, test, time in cases) + report = tmp_path / name + report.write_text(f'{body}') + return report + + +def _refresh(table: Path, root: Path, *reports: Path) -> subprocess.CompletedProcess: + return subprocess.run([sys.executable, str(SCRIPT), *map(str, reports), "--table", str(table), "--root", str(root)], + capture_output=True, text=True) + + +def test_sums_each_files_tests_and_keeps_the_split_files_tests_apart(tmp_path): + table = _checkout(tmp_path) + one = _report(tmp_path, "one.xml", [("tst.integration.cli.cli_test.TestCliIntegration", "test_deploy", 30.0), + ("tst.integration.cli.cli_test.TestCliIntegration", "test_put", 12.5)]) + two = _report(tmp_path, "two.xml", [("tst.integration.env.gateway.gateway_test", "test_mode[scale]", 41.0), + ("tst.integration.env.gateway.gateway_test", "test_other", 7.0), + ("tst.integration.env.task_step.task_steps_test", "test_load", 3.0)]) + result = _refresh(table, tmp_path, one, two) + assert result.returncode == 0, result.stdout + written = json.loads(table.read_text()) + assert written["seconds"] == { + "tst/integration/cli/cli_test.py": 42.5, + f"{SPLIT}::test_mode[scale]": 41.0, + f"{SPLIT}::test_other": 7.0, + "tst/integration/env/task_step/task_steps_test.py": 3.0, + } + assert written["split_by_test"] == [SPLIT] and written["_comment"] == "kept" + + +def test_a_case_whose_file_is_gone_is_left_out(tmp_path): + table = _checkout(tmp_path) + report = _report(tmp_path, "r.xml", [("tst.integration.cli.cli_test", "test_deploy", 5.0), + ("tst.integration.renamed_test", "test_x", 99.0)]) + result = _refresh(table, tmp_path, report) + assert result.returncode == 0 + assert "tst.integration.renamed_test" in result.stdout + assert json.loads(table.read_text())["seconds"] == {"tst/integration/cli/cli_test.py": 5.0} + + +def test_reports_with_no_cases_leave_the_table_as_it_was(tmp_path): + table = _checkout(tmp_path) + before = table.read_text() + result = _refresh(table, tmp_path, _report(tmp_path, "empty.xml", [])) + assert result.returncode == 1 + assert table.read_text() == before diff --git a/tst/util/shards.py b/tst/util/shards.py new file mode 100644 index 000000000..70c0288c5 --- /dev/null +++ b/tst/util/shards.py @@ -0,0 +1,93 @@ +"""Split the selected tests across CI runners: ``AGENTENV_TEST_SHARD=i/N`` keeps the i-th of N groups. + +A file's tests stay together, so its module fixtures (image builds, deployed envs) run once per runner; +a file the durations table names in ``split_by_test`` is split by test instead, except that the tests +``pytest.mark.dependency`` links stay on one runner. The groups are balanced by the seconds the table +records (``tst/integration/slow_durations.json``), a unit it doesn't know weighs the median of the ones it +does, and the same table and selection always give the same groups. +""" + +from __future__ import annotations + +import json +import statistics +from collections import defaultdict +from pathlib import Path + +ENV_VAR = "AGENTENV_TEST_SHARD" +DURATIONS = Path(__file__).resolve().parents[1] / "integration" / "slow_durations.json" + + +def parse(value: str) -> tuple[int, int]: + """``"i/N"`` as ``(i, N)``, with ``1 <= i <= N``.""" + try: + index, count = (int(part) for part in value.split("/")) + except ValueError: + raise ValueError(f"{ENV_VAR} must look like 2/4, not {value!r}") from None + if not 1 <= index <= count: + raise ValueError(f"{ENV_VAR}={value}: the shard must be between 1 and {count}") + return index, count + + +def select(nodeids: list[str], index: int, count: int, table: dict, together: dict[str, str] | None = None) -> list[str]: + """The nodeids that shard ``index`` of ``count`` runs, in their collected order. ``together`` maps a nodeid + to a unit it shares with others (``dependency_groups``); that unit weighs the sum of its tests.""" + split = set(table.get("split_by_test", [])) + seconds = table.get("seconds", {}) + base = {nodeid: _unit(nodeid, split) for nodeid in nodeids} + unit = {nodeid: (together or {}).get(nodeid, base[nodeid]) for nodeid in nodeids} + known = [seconds[name] for name in set(base.values()) if seconds.get(name, 0) > 0] + default = statistics.median(known) if known else 1.0 + weights: dict[str, float] = defaultdict(float) + for base_unit, shared in {base[nodeid]: unit[nodeid] for nodeid in nodeids}.items(): + weights[shared] += seconds.get(base_unit, default) + shard = _assign(weights, count) + return [nodeid for nodeid in nodeids if shard[unit[nodeid]] == index] + + +def dependency_groups(tests: list[tuple[str, str, list[str]]], table: dict) -> dict[str, str]: + """The unit each test of a ``split_by_test`` file shares with the tests ``pytest.mark.dependency`` links + it to, as ``select``'s ``together``. ``tests`` are (nodeid, the name its marker gives it, the names it + depends on), for the tests with the marker; a dependency is looked up in the test's own file.""" + split = set(table.get("split_by_test", [])) + parent: dict[str, str] = {} + + def root(nodeid: str) -> str: + while parent.get(nodeid, nodeid) != nodeid: + nodeid = parent[nodeid] + return nodeid + + named = {(nodeid.split("::", 1)[0], name): nodeid for nodeid, name, _ in tests} + for nodeid, _, depends in tests: + path = nodeid.split("::", 1)[0] + if path not in split: + continue + for name in depends: + other = named.get((path, name)) + if other is not None: + parent[root(nodeid)] = root(other) + linked = {nodeid for nodeid, _, _ in tests if nodeid.split("::", 1)[0] in split} + groups = defaultdict(list) + for nodeid in linked: + groups[root(nodeid)].append(nodeid) + return {nodeid: min(members) for members in groups.values() if len(members) > 1 for nodeid in members} + + +def load_table(path: Path = DURATIONS) -> dict: + return json.loads(path.read_text()) + + +def _unit(nodeid: str, split_by_test: set[str]) -> str: + path = nodeid.split("::", 1)[0] + return nodeid if path in split_by_test else path + + +def _assign(weights: dict[str, float], count: int) -> dict[str, int]: + """Each unit's shard, from 1: the heaviest unit first, onto the lightest shard so far.""" + loads = [0.0] * count + shard = {} + for unit in sorted(weights, key=lambda name: (-weights[name], name)): + lightest = min(range(count), key=lambda i: (loads[i], i)) + shard[unit] = lightest + 1 + loads[lightest] += weights[unit] + return shard